首頁 > 軟體

C++超詳細講解單連結串列的實現

2022-03-30 19:02:34

單連結串列的實現(從入門到熟練)

概念和結構

概念:連結串列是一種物理儲存結構上非連續、非順序的儲存結構

資料元素的邏輯順序是通過連結串列中的指標鏈 接次序實現的

圖示:

注意:

連結串列結構在邏輯上為連續的,但是物理上(記憶體中)不一定連續

連結串列節點都是在堆上申請出來的,申請空間按一定策略分配

結構種類

連結串列具有多種結構:單向雙向,帶頭不帶頭,迴圈非迴圈

實際上最常用的是:無頭單向非迴圈連結串列,帶頭雙向迴圈連結串列

連結串列的實現

注意:這裡實現的是無頭單向非迴圈連結串列

增刪查改介面

// 動態申請一個節點
SListNode* BuySListNode(SLTDateType x);
// 單連結串列列印
void SListPrint(SListNode* plist);
// 單連結串列尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 單連結串列的頭插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 單連結串列的尾刪
void SListPopBack(SListNode** pplist);
// 單連結串列頭刪
void SListPopFront(SListNode** pplist);
// 單連結串列查詢
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 單連結串列在pos位置之後插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 單連結串列刪除pos位置之後的值
void SListEraseAfter(SListNode* pos);

節點結構體建立

typedef int SLTDateType;
typedef struct SListNode
{
 SLTDateType data;
 struct SListNode* next; 
}SListNode;

節點開闢

對於連結串列來說,每需要空間就需要進行開闢,這裡我們將之封裝成一個函數,便於後續呼叫直接使用(開闢的同時進行賦值)

參考程式碼:

//連結串列節點開闢
SLTNode* BuySListNode(SLTDateType x)
{
	//動態分配一個節點
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		//分配失敗則列印錯誤資訊並結束程序
		perror("newnode fail:");
		exit(-1);
	}
	//成功則進行賦值
	newnode->data = x;
	newnode->next = NULL;
	//返回節點地址,以便後續操作
	return newnode;
}

資料列印

注意:

1.對於不帶頭的連結串列來說,列印資料不需要修改連結串列首節點地址(故只要傳連結串列指標)

2.用迴圈遍歷連結串列,每列印資料,需要指向下一個節點

3.依靠尾節點的址域為NULL來結束迴圈

程式碼:

//連結串列資料列印
void SListPrint(SLTNode* phead)//一級指標接收一級指標(列印不需改變指標本身內容)
{
	//建立一個定址指標
	SLTNode* cur = phead;
	while (cur!=NULL)//後續還有節點
	{
		//列印資料並指向下一個節點
		printf("%d->", cur->data);
		cur = cur->next;
	}
	//最後列印空指標
	printf("NULLn");
	return;
}

連結串列尾插資料

  • 要尾插資料則需要遍歷找到連結串列的尾節點
  • 對於不帶頭連結串列,尾插資料也可能是插在連結串列首元素的地址(當連結串列為空),需要修改連結串列指標的內容(故需要傳入連結串列指標的地址)
  • 插入資料要開闢節點

程式碼:

//連結串列尾插資料
void SListPushBack(SLTNode** pphead, SLTDateType x)//二級指標接收一級指標(尾插存在需改變連結串列指標本身內容)
{
	//避免傳入錯誤(直接報錯便於找到錯誤位置)
	assert(pphead);
	//接收新節點的地址
	SLTNode* newnode=BuySListNode(x);
	//頭節點為空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾節點
		SLTNode* tail = *pphead;//建立定址節點
		//兩個及以上節點的情況
		while (tail->next != NULL)
		{
			//指向下一個節點
			tail = tail->next;	
		}
		//找到了
		tail->next = newnode;
	}
	return;
}

注意程式碼中的assert的作用:

  • 正確傳入連結串列指標的地址是不會為空的
  • 但是對於非正常傳入連結串列指標(不是連結串列指標的地址)且此時連結串列指標為空則會發生報錯(assert報錯會告訴錯誤位置),告訴程式設計師應該傳入連結串列指標的地址

頭刪

注意:

  • 刪除前需要儲存當前節點的址域(即儲存下個節點的空間地址,以防丟失)
  • 前刪資料即刪除當前連結串列首節點,即需修改連結串列指標的內容(故需傳入連結串列指標的地址)
  • 刪除後修改連結串列指標內容,指向新的首節點
  • 如果連結串列為空時無法刪除(儲存下個節點地址會造成非法存取)

程式碼:

//連結串列前刪資料
void SListPopFront(SLTNode** pphead)
{
	//避免傳入錯誤(直接報錯便於找到錯誤位置)
	assert(pphead);
	//連結串列為空的情況
	if (*pphead == NULL)
	{
		return;
	}
	//建立指標儲存第二個節點地址
	SLTNode* next = (*pphead)->next;
	//釋放當前頭結點空間
	free(*pphead);
	//更新頭結點資料
	*pphead = next;
	return;
}

連結串列資料查詢

注意:

  • 查詢時用迴圈遍歷連結串列
  • 對於查詢資料不用修改連結串列指標的內容,故只需傳入連結串列指標就行了
  • 查詢到時則返回節點地址,否則返回NULL

程式碼:

//連結串列資料查詢
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	//建立定址指標
	SLTNode* cur = phead;
	while (cur!=NULL)
	{
		if (cur->data == x)//找到資料符合節點
		{
			return cur;//返回節點地址(好處:以便後續再尋找或修改)
		}
		else
		{
			//找下一個節點
			cur = cur->next;
		}
	}
	//未找到資料符合節點
	return NULL;
}

連結串列pos位置前插資料

注意:

  • 想要pos位置前插資料,不僅需要找到pos位置,還需要記錄pos的前一個節點位置
  • 傳入的pos為NULL則報錯
  • 進行修改前節點的址域成新節點,並讓新節點的址域修改成pos,這才是一個成功的pos位置前插資料
  • 迴圈遍歷連結串列查詢pos位置,沒有找到pos位置則什麼也不幹

程式碼:

//連結串列pos位置往前插入(較難)(還有在pos位置之後插入,簡單點)
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	//避免傳入錯誤(直接報錯便於找到錯誤位置)
	assert(pphead);
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	//首結點符合的情況
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		//建立定址指標
		SLTNode* cur = *pphead;
		while (cur !=NULL)
		{
			if (cur->next!= pos)
			{
				cur = cur->next;//找到下一節點
			}
			else // 找到對應空間
			{
				cur->next = newnode;
				newnode->next = pos;
				return;//結束尋找(否則會一直插入,造成死迴圈)
			}
		}
	}
	//未找到則什麼也不幹
	return;
}

連結串列pos位置後插資料

注意:

  • 後插則不用關注是否為首節點
  • 也不用找到遍歷找到前節點的位置
  • 後插則先將新節點址域改成pos後節點地址再將pos的址域改成新節點地址

ps:一定要注意修改連結節點址域的先後,避免地址的丟失

程式碼:

//連結串列pos位置往後插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
	return;
}

連結串列pos位置資料刪除

注意:

  • 考慮刪除首節點的情況,可能修改連結串列指標的內容,故需要傳入連結串列指標的地址
  • 對於刪除節點,需要先儲存pos位置下一個節點地址,將pos位置釋放,再將pos位置前節點的址域改成pos位置後節點的地址,這才是成功的刪除pos位置節點
  • 迴圈找pos位置,沒找到則什麼也不幹

參考程式碼:

//連結串列pos位置刪除
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	//避免傳入錯誤(直接報錯便於找到錯誤位置)
	assert(pphead);
	assert(pos);
	//頭結點符合的情況
	if (*pphead == pos)
	{
		*pphead = (*pphead)->next;
		free(pos);
	}
	else
	{
		//建立定址指標
		SLTNode* cur = *pphead;
		while (cur != NULL)
		{
			if (cur->next != pos)
			{
				cur = cur->next;//找到下一節點
			}
			else // 找到對應空間
			{
				SLTNode* next = cur->next->next;
				free(cur->next);
				cur->next = next;
				return;//結束尋找
			}
		}
	}
	//未找到則什麼也不幹
	return;
}

連結串列節點釋放

注意:

  • 對於動態開闢的記憶體空間,在使用後一定要記得的進行釋放(避免造成記憶體漏失)
  • 因為連結串列節點是一個個開闢的,同樣的釋放也需要一個個進行釋放
  • 迴圈遍歷釋放當前節點前需儲存後一個節點的地址,避免地址丟失無法釋放
  • 釋放完後,還需將連結串列指標給置空(避免使用野指標)

參考程式碼:

//連結串列節點釋放
void SListDestory(SLTNode** pphead)
{
	//避免傳入錯誤(直接報錯便於找到錯誤位置)
	assert(pphead);
	//遍歷釋放
	SLTNode* cur = *pphead;
	while (cur)
	{
		//儲存下一個地址
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	//置空
	*pphead = NULL;
	return;
}

連結串列與順序表區別

連結串列的優缺點

優點

  • 按需申請空間(空間使用合理)
  • 插入效率高(不用像順序表樣挪動資料)

缺點

  • 不支援隨機存取(無法用下標直接存取)

順序表的優缺點

優點

  • 支援隨機存取 (有些演演算法需要結構支援隨機存取:二分查詢,快排等)

缺點

  • 擴容空間有消耗(空間碎片化以及空間浪費)
  • 插入資料需要挪動資料有消耗

到此這篇關於C++超詳細講解單連結串列的實現的文章就介紹到這了,更多相關C++ 單連結串列內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com