1. 程式人生 > >如何使用C++實現單鏈表

如何使用C++實現單鏈表

為什麼假期也在發文章

//TODO NullGirlfrindException

請忽略以上兩行無聊的事實......

如何彌補順序表的不足之處?

第一次學習線性表一定會馬上接觸到一種叫做順序表(順序儲存結構),經過上一篇的分析順序表的優缺點是很顯然的,它雖然能夠很快的訪問讀取元素,但是在解決如插入和刪除等操作的時候,卻需要移動大量的元素,效率較低,那麼是否有一種方法可以改善或者解決這個問題呢?

首先我們需要考慮,為什麼順序表中的插入刪除操作會涉及到元素的移動呢?

好傢伙,問題就是圍繞著順序表的最大的特點出現的——順序儲存,相鄰放置元素,也就是說每個元素都是根據編號一個一個挨著的,這就導致了 插入或刪除後,為了仍然呈順序線性儲存,被操作元素後面的元素的位置均需要發生一定的變化,你應該能想象得到,在擁擠的隊伍中突然從中插入一個學生的場景,後面浩浩蕩蕩的人群,口吐芬芳的向後挪了一個空位,如果人群過大,重新排好隊也需要一定的時間

好嘛,人與人之間別這麼擠在一起,每個人與人之間都流出一點空隙來,留一定的位置出來,好了,這好像是個辦法,但是負責一個一個與學生交流填表的老師可就不幹了,這意味著我(找人)遍歷的時候,需要多跑好多路,浪費好多時間,先不說這個,體院館又不行了,你們這麼個擺法,我這小館可放不下,這也就意味著空間複雜度增加了很多

我們剛才所圍繞的都是在 "排隊" 的基本前提下的,但我們能想到的方法並不是很理想,那麼我們索性就不排隊了,是不是能有更好的解決方式呢?

一個有效的方法:

讓同學們(元素)自己找位置隨便站,不過你要知道相對於自己下一位同學的位置,這樣既解決了空間上的問題,又能通過這種兩兩聯絡的方式訪問(遍歷)到整個隊伍(陣列),最重要的是,插入和離開同學,由於同學(元素)之間不存在了那種排隊,相鄰的特點,所以也不會說影響到過多的同學(元素)只需要和你插入位置的前後兩位同學溝通好就行了,反正別人也不知道你們之間發生了什麼事

好了思路是有了,我們來看一種最常見的連結串列——單鏈表

單鏈表的基本結構

這種連結串列為什麼被稱作單鏈表呢?這是因為它只含有一個地址域,這是什麼意思呢?

我們在連結串列中擯棄了順序表中那種一板一眼的排隊方式,但是我們必須讓兩個應該相鄰的元素之間有一定的相互關係,所以我們選擇讓每一個元素可以聯絡對應的下一個元素

而這個時候我們就需要給每個元素安排一個額外的位置,來儲存它的後繼元素的儲存地址,這個儲存元素資訊的域叫做指標域或地址域,指標域中儲存的資訊也叫作指標或者鏈

我們用一張圖 看一下他的結構

結構中名詞解釋

  • 頭指標:一個指向第一個節點地址的指標變數

    • 頭指標具有標識單鏈表的作用,所以經常用頭指標代表單鏈表的名字
  • 頭結點:在單鏈表的第一個結點之前附設一個結點,它沒有直接前驅,稱之為頭結點

    • 可不存資訊,也可以作為監視哨,或用於存放線性表的長度等附加資訊
    • 指標域中存放首元結點的地址
  • 首元結點:儲存第一個元素的節點

為什麼要附設一個頭結點

我們來解釋一下:

  • 連結串列如果為空的情況下,如果單鏈表沒有頭結點,那麼頭指標就會指向NULL,如果加上頭結點,無論單鏈表是否為空,頭指標都會指向頭結點,這樣使得空連結串列與非空連結串列處理一致

  • 使首元結點前插入或刪除元素的時候,與後面操作相同,不需要產生額外的判斷分支,使得演算法更加簡單

(以插入為例講解)在帶頭結點的情況下,在首元結點前插入或者刪除元素仍與在其他位置的操作相同,只需要將前一個元素(在這裡是頭結點)的指標域指向插入元素,同時將插入元素的指標域指向原來的第二的元素

而無頭結點的情況由於,首元結點前沒有元素,只能通過修改head的前後關係,所以導致了 與在別的位置插入或刪除元素的操作不同,在實現這兩個功能的時候就需要額外的寫一個判斷語句來判斷插入的位置是不是首元結點之前的位置,增加了分支,程式碼不夠簡潔

總結:頭結點的存在使得空連結串列與非空連結串列處理一致,也方便對連結串列首元結點前結點的插入或刪除操作

單鏈表的型別定義

###線性表的抽象資料型別定義

我們在給出單鏈表的定義之前我們還是需要先引入我們線性表的抽象資料型別定義

#ifndef _LIST_H_
#define _LIST_H_
#include<iostream>
using namespace std;

class outOfRange{};
class badSize{};
template<class T>
class List {
public:
    // 清空線性表
	virtual void clear()=0;
    // 判空,表空返回true,非空返回false
	virtual bool empty()const=0;
    // 求線性表的長度
	virtual int size()const=0;
    // 線上性表中,位序為i[0..n]的位置插入元素value
	virtual void insert(int i,const T &value)=0;
    // 線上性表中,位序為i[0..n-1]的位置刪除元素
	virtual void remove(int i)=0;
    // 線上性表中,查詢值為value的元素第一次出現的位序
	virtual int search(const T&value)const=0;
    // 線上性表中,查詢位序為i的元素並返回其值
	virtual T visit(int i)const=0;
    // 遍歷線性表
	virtual void traverse()const=0;
    // 逆置線性表
	virtual void inverse()=0;					
	virtual ~List(){};
};

/*自定義異常處理類*/ 


class outOfRange :public exception {  //用於檢查範圍的有效性
public:
	const char* what() const throw() {
		return "ERROR! OUT OF RANGE.\n";
	}
};

class badSize :public exception {   //用於檢查長度的有效性
public:
	const char* what() const throw() {
		return "ERROR! BAD SIZE.\n";
	}
};

#endif

單鏈表的型別定義

#ifndef _SEQLIST_H_
#define _SEQLIST_H_
#include "List.h"
#include<iostream>
using namespace std;

template<class elemType>
//elemType為單鏈表儲存元素型別 
class linkList:public List<elemType> {
private:
	//節點型別定義 
	struct Node {
		//節點的資料域 
		elemType data;
		//節點的指標域 
		Node *next;
		//兩個建構函式 
		Node(const elemType value, Node *p = NULL) {
			data = value;
			next = p;
		} 
		Node(Node *p = NULL) {
			next = p;
		} 
	};
	
	//單鏈表的頭指標 
	Node *head;
	//單鏈表的尾指標 
	Node *tail;
	//單鏈表的當前長度 
	int curLength;
	//返回指向位序為i的節點的指標 
	Node *getPostion(int i)const; 
public:
	linkList();
	~linkList();
	//清空單鏈表,使其成為空表 
	void clear();
	//帶頭結點的單鏈表,判空 
	bool empty()const {return head -> next == NULL;} 
	//返回單鏈表的當前實際長度
	int size()const {return curLength;}
	//在位序i處插入值為value的節點表長增1 
	void insert(int i, const elemType &value); 
	//刪除位序為i處的節點,表長減1
	int search(const elemType&value)const;
	//查詢值為value的節點的前驅的位序
	int prior(const elemType&value)const;
	//訪問位序為i的節點的值,0定位到首元結點
	elemType visit(int i)const;
	//遍歷單鏈表
	void traverse()const;
	//頭插法建立單鏈表
	void headCreate();
	//尾插法建立單鏈表
	void tailCreate();
	//逆置單鏈表 
	void inverse();
};

單鏈表上的基本運算實現

(一) 單鏈表的初始化-建構函式

單鏈表的初始化就是建立一個帶頭節點空連結串列,我們不需要設定其指標域,為空即可

template<class elemType>
linkList<elemType>::linkList() {
	head = tail = new Node();
	curLength=0;
}

注意:new 操作符代表申請堆記憶體空間,上述程式碼中應該判斷是否申請成功,為簡單,預設為申請成功,實際上如果系統沒有足夠的記憶體可供使用,那麼在申請記憶體的時候會報出一個 bad_alloc exception 異常

(二) 解構函式

當單鏈表物件脫離其作用域時,系統自動執行解構函式來釋放單鏈表空間,其實也就是清空單鏈表內容,同時釋放頭結點

template<class elemType>
linkList<elemType>::~linkList() {
	clear();
	delete head;
}

(三) 清空單鏈表

清空單鏈表的主要思想就是從頭結點開始逐步將後面節點釋放掉,但是我們又不想輕易的修改頭指標head的指向,所以我們引入一個工作指標,從頭結點一直移動到表尾,逐步釋放節點

template<class elemType>
void linkList<elemType>::clear() {
	Node *p, *tmp;
	p - head -> next;
	while(p != NULL) {
		tmp = p;
		p = p -> next();
		delete tmp; 
	}
	head -> next = NULL;
	tail = head;
	curLength = 0;	
}

(四) 求表長

由於我們的程式碼中已經定義過一個叫做 curLength 的變數用來記錄我們的表長

所以我們可以直接返回,我們在定義中已經實現了,也就是這句

//返回單鏈表的當前實際長度
int size()const {return curLength;}

但是如果我們沒有這樣一個變數,我們想要實現這樣的功能又是什麼樣的方法呢?

template<class elemType>
int linkList<elemType>::size()const {
	Node *p = head -> next;
	int count;
	while(p) {count++; p = p -> next;}
	return count;
}

(五) 遍歷單鏈表

我們需要從頭到尾訪問單鏈表中的每一個節點,並且輸出其中資料域的資訊

template<class elemType>
void linkList<elemType>::traverse()const {
	Node *p = head -> next;
	cout << "traverse:";
	while (p != NULL) {
		cout << p -> date << " ";
		p = p -> next;
	}
}

(六) 按照位序 i 尋找其元素對應記憶體地址

設定一個移動工作指標,和一個計數器 count,初始時p指向頭結點,每當指標p移向下一個結點的時候,計數器count + 1 ,直到 p指向位序為 i的節點為止。返回 p

template<class elemType>
typename linkList<elemType>::Node *linkList<elemType>::getPostion(int i)const {
	if(i < -1 || i > curLength - 1)
		return NULL;
	Node *p = head;
	int count = 0;
	while(count <= i) {
		p = p -> next;
		count++;
	}
	return p;
}

(七) 按值查詢節點位序

設定一個移動工作指標,和一個計數器 count,從單鏈表的第一個節點開始,開始於給定的值進行比對,如果相等則查詢成功,返回節點的位序,否則繼續查詢知道單鏈表結束,查詢失敗返回 -1

template<class elemType>
int linkList<elemType>::search(const elemType&value)const {
	Node *p = head -> next;
	int count = 0; 
	while (p != NULL && p -> data != value) {
		p = p -> next;
		count++;
	}
	if (p == NULL) {
		return -1;
	}else {
		return count; 
	}
}

(八) 插入節點

在位序為 i 出插入值為value 的新節點q,我們需要做的就是找到位序為i - 1 的節點p,讓q指標域指向原來p的後繼,然後修改p的後繼為q即可,說白了也就是修改插入元素位置前後的元素指向關係就可以了

template<class elemType>
void linkList<elemType>::insert(int i,const elemType &value) {
	Node *p, *q;
	if(i < 0 || i > curLength)
		throw outOfRange();
	p = getPostion(i - 1);
	q = new Node(value,p -> next);
	p -> next = q;
	if (p == tail) tail = q;
	curLength++;
}

(九) 刪除節點

能看懂新增節點的方法,理解刪除節點也是手到擒來

template<class elemType>
void linkList<elemType>::remove(int i) {
	//p是待刪節點,pre是其前驅 
	Node *p, *pre;
	if(i < 0 || i > curLength)
		throw outOfRange();
	pre = getPostion(i - 1);
	p = pre -> next;
	if (p == tail) {
		tail = pre;
		pre -> next = NULL;
		delete p;
	} else {
		pre -> next = p -> next;
		delete p;
	}
}

單鏈表整表的建立

回顧我們前面認識的順序表,它其實可以理解為一個數組,我們宣告一個型別,同時給定值,初始化其大小,但是單鏈表就不一樣了,它是一種動態組織,它不需要像順序表一樣元素集中,它可以隨著實際的情況來動態生成節點,所以也不需要預先分配空間大小和位置

(一) 頭插法建立單鏈表

頭插法的意思就是說,每次新增節點全部插在頭結點之後,首元結點之前,你可以這樣理解,我先來排隊,但是後面來了人,他就會排到我的前面去,我們來藉助圖看一下

我們一次插入元素 123 但實際上輸出的是按照321的順序儲存的,也就是說和我們的邏輯順序是相反的

我們來看一看怎麼實現它

template<class elemType>
void linkList<elemType>::headCreate() {
	Node *p;
	elemType value, flag;
	cout << "inputelements, ended with:";
	cin >> flag;
	while(cin >> value, value != flag) {
		//p -> data == value, p -> next = head ->next 
		p = new Node(value, head -> next);
		head -> next = p;
		//原連結串列為空,新節點p成為為節點 
		if (head == tail) 
			tail = p;
		curLength++; 
	}
}

逆置單鏈表

我們知道單鏈表中元素順序與讀入的順序是相反的,我們可以通過逆置單鏈表的演算法,幫助我們重新恢復我們的慣有思維順序

template<class elemType>
void linkList<elemType>::inverse() {
	Node *p, *tmp;
	//p為工作指標,指向首元結點 
	p = head -> next;
	//頭結點的指標域置空,構成空連結串列 
	head -> next = NULL;
	//逆置後首元結點將成為尾節點 
	if (p)
		tail = p;
	while (p) {
		//暫存p的後繼 
		tmp = p -> next;
		p -> next = head -> next;
		//節點p插在頭結點的後面 
		head -> next = p;
		//繼續處理下一個節點 
		p = tmp; 
	}
}

(二) 尾插法建立單鏈表

看完了頭插法,但是感覺這樣的順序與我們一貫的思維總是有一點彆扭,而尾插法則是一種,邏輯順序與我們一致的建立方法

還是看一下圖

template<class elemType>
void linkList<elemType>::tailCreate() {
	Node *p;
	elemType value, flag;
	cout << "inputelements, ended with:";
	cin >> flag;
	while(cin >> value, value != flag) {
		p = new Node(value,NULL);
		tail -> next = p;
		tail = p;
		curLength++;
	}
}

合併單鏈表

要求:假設我們給出兩個仍然是遞增的單鏈表la和lb,我們將其合併為lc 仍保證遞增,利用原表空間,但是我們仍在下面將表C稱作新表

因為我們的要求是遞增的,所以使用尾插法是非常合適的,我們設計三個工作指標,分別指向兩個表的首元結點,然後將第三個指標指向新表的頭結點,比較前兩個指標指向的值,小的就放到新表的表尾,然後後移動兩表中較小的那一個的指標,以此類推,直到其中一個表尾空,將剩餘的節點全部連結到新表的末尾

template<class elemType>
typename linkList<elemType> *linkList<elemType> ::Union(linkList<elemType> *lb) {
	Node *pa, *pb, *pc;
	linkList<elemType> *lc = this;
	pa = head -> next;
	head -> next = NULL;
	pb = (lb -> head) -> next;
	(lb -> head) -> next = NULL;
	
	pc = lc -> head;
	while(pa && pb) {
		if(pa -> data <= pb -> data) {
			pc-> next = pa;
			pc = pa;
			pa = pa -> next;
		} else {
			pc -> next = pb;
			pc = pb;
			pb = pb -> next;
		}
	}
	if(pa) {
		pc -> next = pa;
		lc -> tail = tail;
	} else {
		pc -> next = pb;
		lc -> tail = lb -> tail;
	}
	lc -> cuirLength = curLength + lb -> curLength;
	delete lb;
	return lc; 
}

總結

單鏈表,採取了鏈式儲存結構,用一組任意的儲存單元存放線性表的元素,尤其對於需要頻繁的插入和刪除資料的時候更加適用,如果需要進行頻繁的查詢還是推薦使用順序表,例如對於一個學生成績管理系統的製作,學生更多的時候是檢視自己的成績,而錄入的老師,也只有在考試後錄入一次,所以應該使用順序表,而例如考勤打卡系統,更多的是打卡資訊的記錄,所以還是選擇使用連結串列,當然例子可能不是很恰當,同時正常的開發中還會有更多複雜的問題需要考慮,舉例子只為了利於理解

結尾:

如果文章中有什麼不足,或者錯誤的地方,歡迎大家留言分享想法,感謝朋友們的支援!

如果能幫到你的話,那就來關注我吧!如果您更喜歡微信文章的閱讀方式,可以關注我的公眾號

在這裡的我們素不相識,卻都在為了自己的夢而努力 ❤

一個堅持推送原創開發技術文章的公眾號:理想二旬不止

相關推薦

C++實現單鏈

       太簡單了,直接貼題目然後上程式碼。        題目: 實驗2 2.1 實驗目的 熟練掌握線性表的鏈式儲存結構。 熟練掌握單鏈表的有關演算法設計。 根據具體問題的

資料結構實驗2:C++實現單鏈

       太簡單了,沒啥可說的,程式碼意義明白如話。        題目與要求:                                                          實驗2      2.1 實驗目的        熟練掌握線

C++實現單鏈的逆轉

答: 方法一:把連結串列中各結點的next域指向其前驅結點,並將原先第一個結點的next域設為NULL,將head指向原先連結串列的最後一個結點,其具體實現如下: LinkNode* ReverseLink(LinkNode **head) {LinkNode *cur,*

C++實現單鏈的12種基本操作

C++單鏈表的操作 2017-12-25 // 單鏈表.cpp: 定義控制檯應用程式的入口點。 //Author:kgvito //Date: 2017.12.25 #include "stdafx.h" #include<iostream> usin

c++實現單鏈基本操作

程式媛決定好好學習寫程式碼 連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列結點(連結串列中每一個元素稱為結點)組成,結點可以在執行時動態生成。每個結點包括兩個部分:一個是儲存資料元素的資料域,

C++實現單鏈的逆置

#include<iostream> using namespace std; const int N=6; typedef struct node{ //單鏈表 int data; struct node* next

C++實現單鏈(不含頭結點)

 VS2005執行通過,如有問題,請各位大牛指正。 注意:單鏈表不含有頭結點 #include <iostream> using namespace std; template<class Type> //定義結點 struct Node { T

C++實現單鏈的建立、逆置和輸出

題目描述:在已知單鏈表頭節點的情況下,設計演算法逆置單鏈表並輸出 方法一:採用首先將頭節點指向空,讓其變為尾節點,然後利用中間節點 p、q 將其後的節點一個接一個改為指向前面的節點 /******

c++實現單鏈的各種操作

在下例中,演示了單鏈表的各種操作 #include <iostream> using namespace std; typedef struct Node { int data; //資料域 struct Node * next; //指標域

如何使用C++實現單鏈

為什麼假期也在發文章 //TODO NullGirlfrindException 請忽略以上兩行無聊的事實...... 如何彌補順序表的不足之處? 第一次學習線性表一定會馬上接觸到一種叫做順序表(順序儲存結構),經過上一篇的分析順序表的優缺點是很顯然的,它雖然能夠很快的訪問讀取元素,但是在解決如插入和刪除等

C語言實現單鏈節點的刪除(帶頭結點)

data art pos grand urn ria tps move sni 我在之前一篇博客《C語言實現單鏈表節點的刪除(不帶頭結點)》中具體實現了怎樣在一個不帶頭結點的單鏈表的刪除一個節點,在這一篇博客中我改成了帶頭結點的單鏈表。代碼演示樣例上傳至 h

C語言實現單鏈的節點插入(帶頭結點)

alloc tails 函數 file ret con 實現 單獨 fun 我在之前一篇博客《C語言實現單鏈表(不帶頭結點)節點的插入》中具體實現了怎樣在一個不帶頭結點的單鏈表中進行節點的插入。可是在實際應用中,帶頭結點的鏈表更為經常使用。更為方便。今天我們

C++實現線性的鏈接存儲結構(單鏈

mage pac lis ins 尾插 初始化 arr space ios 將線性表的抽象數據類型定義在鏈接存儲結構下用C++的類實現,由於線性表的數據元素類型不確定,所以采用模板機制。 1 頭文件linklist.h 2 #pragma once 3 #in

c語言實現單鏈的逆序輸出

<span style="font-family: Arial, Helvetica, sans-serif;">可以用遞迴,如果沒到連結串列尾,則遞迴查詢,否則輸出當前值。下面只是演算法表示,不能直接放到程式裡編譯執行。</span><span style="fo

c語言實現單鏈的所有介面

此次工程還是使用了3個原始檔slist.h(標頭檔案原始碼),slist.c(實現介面的具體程式碼),test.c(單鏈表邏輯) slist.h #pragma once #include<stdio.h> #include<stdlib.h> #inclu

資料結構之鏈式實現--單鏈(C語言)

學習參考: 嚴蔚敏: 《資料結構-C語言版》 基本操作: 單鏈表的建立 新增結點(頭插法) 新增結點(尾插法) 單鏈表的輸出 單鏈表的修改 單鏈表的插入 單鏈表的刪除 單鏈表按

C語言實現單鏈反轉

最近在考研複習,記錄一下基礎的資料結構演算法,有事沒事翻一翻,以防忘了 自己寫了個翻轉連結串列演算法,感覺要比別人的要通俗易懂些 void Reverse(List *L){ //分別是當前節點,直接前驅節點,直接後繼節點 LNode *current,

C語言單鏈及基本操作的實現

        資料結構中,單向連結串列(又名單鏈表、線性連結串列)是連結串列的一種,其特點是連結串列的連結方向是單向的,對連結串列的訪問要通過從頭部開始,依序往下讀取。         下面的程式碼是使用指標實現的

C語言單鏈實現19個功能完全詳解

#include "stdafx.h" #include "stdio.h" #include <stdlib.h> #include "string.h"   typedef int elemType ;   /************************

C單鏈的簡單實現和應用!!!

在單鏈表裡面,每個節點包含一個指向下一個節點 的指標。連結串列的最後一個節點的指標欄位是一個值為NULL的指標,他的作用就是提示連結串列後面不再有其他的節點。在你找到連結串列的第一個節點的時候呼叫節點裡面的指標就可以依次訪問剩下所有的節點。為了記住連結串列的起始位置,可以用