探索資料結構:單連結串列的實戰指南

Betty’sSweet發表於2024-03-09


✨✨ 歡迎大家來到貝蒂大講堂✨✨

🎈🎈養成好習慣,先贊後看哦~🎈🎈

所屬專欄:資料結構與演算法
貝蒂的主頁:Betty‘s blog

前言

在上一章節中我們講解了資料結構中的順序表,知道了順序表的空間是連續儲存的,這與陣列非常類似,為我們隨機訪問資料提供了便利的條件。但是同時當插入資料時可能存在移動資料與擴容的情況,這大大增加我們的時間與空間成本。為了解決這個問題,就要學習我們今天要講解的連結串列。

1. 什麼是連結串列

連結串列是一種物理儲存結構上非連續、非順序的儲存結構,資料元素的邏輯順序是透過連結串列中的指標連結次序實現的 。與順序表不同,連結串列的儲存資料在記憶體是隨機分佈的。

2. 連結串列的分類

連結串列的種類多種多樣,其中最常見的有八種,它們大致可以分為三類:

2.1 單向或者雙向

2.2 帶不帶頭節點

2.3 迴圈不迴圈

本專欄將選擇其中兩種常見連結串列進行講解,那就是無頭單向非迴圈連結串列與與帶頭雙向迴圈連結串列,這兩種優點主要有:

  • 無頭單向非迴圈連結串列:結構簡單,一般不會單獨用來存資料。實際中更多是作為其他資料結構的子結構,如雜湊桶、圖的鄰接表等等。另外這種結構在筆試面試中出現很多。
  • 帶頭雙向迴圈連結串列:結構最複雜,一般用在單獨儲存資料。實際中使用的連結串列資料結構,都是帶頭雙向迴圈連結串列。另外這個結構雖然結構複雜,但是使用程式碼實現以後會發現結構會帶來很多優勢,實現反而簡單了,後面我們程式碼實現了就知道了。

3. 單連結串列的功能

單連結串列的主要功能有以下幾個:

  1. 列印單連結串列中的資料。
  2. 對單連結串列進行頭插(開頭插入資料)。
  3. 對單連結串列進行頭刪(開頭刪除資料)。
  4. 對單連結串列進行尾插(末尾插入資料)。
  5. 對單連結串列進行尾刪(末尾刪除資料)。
  6. 對單連結串列進行查詢資料。
  7. 對單連結串列資料進行修改。
  8. 任意位置的刪除和插入資料。
  9. 銷燬單連結串列。

4. 單連結串列的定義

單連結串列的結點定義方式與我們以往定義的方式都不同,它是一個結構體中包含兩個成員。一個是儲存數值,一個存放下一個結點的地址。

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

實際記憶體儲存結構:

5. 單連結串列功能實現

5.1 列印單連結串列

列印連結串列需要對函式傳參指向連結串列的指標,首先為了保證連結串列不為空(NULL),需要對其進行斷言。

(1) 程式碼實現

void PrintSList(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

(2) 複雜度分析

  • 時間複雜度:因為需要遍歷整個連結串列,顯然時間複雜度為O(N)。
  • 空間複雜度:列印連結串列不需要格外的空間,所以空間複雜度為O(1).

5.2 單連結串列頭插

(1) 建立結點

單連結串列每次插入都需要插入一個節點,所以我們最好寫一個建立節點的函式方便後續呼叫。

SListNode* SListCreatNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
        	return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

(2) 頭插程式碼實現

單連結串列的頭插與順序表頭插最大的不同就是單連結串列傳參需要二級指標。因為頭插資料需要改變一級指標plist的指向,而形參的改變無法影響實參,所以需要二級指標才能改變一級指標plist的指向。

void SListPushFront(SListNode** plist, SLTDataType x)
{
	assert(plist);
	SListNode* newnode = SListCreatNode(x);
	newnode->next = *plist;
	*plist = newnode;
}

(3) 複雜度分析

  • 時間複雜度:不需要額外時間的消耗,時間複雜度為O(1)。
  • 空間複雜度:固定創造一個節點,空間複雜度為O(1)。

5.3 單連結串列頭刪

頭刪與頭插類似,都可能需要改變plist的指向,所以也需要二級指標。並且也需要防止連結串列為空的情況發生。

(1) 程式碼實現

void SListPopFront(SListNode** plist)
{
	assert(plist);
	assert(*plist);
	SListNode* cur = (*plist)->next;
	free(*plist);
	*plist = cur;
}

(2) 複雜度分析

  • 時間複雜度:不需要額外時間的消耗,時間複雜度為O(1)。
  • 空間複雜度:不需要格外的空間消耗,空間複雜度為O(1)。

5.4 單連結串列尾插

當連結串列為空時,單連結串列尾插就會變成單連結串列頭插,也需要改變plist的指向。其餘情況即可透過迴圈尋找尾節點。

(1) 程式碼實現

void SListPushBack(SListNode** plist, SLTDataType x )
{
	assert(plist);
	if (*plist== NULL)
	{
		*plist = SListCreatNode(x);
	}
	else
	{
		SListNode* ptail = *plist;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = SListCreatNode(x);
	}
}

(2) 複雜度分析

  • 時間複雜度:需要遍歷整個連結串列,時間複雜度為O(N)。
  • 空間複雜度:固定創造一個節點,空間複雜度為O(1)。

5.5 單連結串列尾刪

與單連結串列尾插類似,當連結串列只有一個頭結點時需要將plist置為空,所以也需要二級指標。並且也需要防止連結串列為空的情況發生。

(1) 程式碼實現

void SListPopBack(SListNode** plist)
{
	assert(plist);
	assert(*plist);//防止連結串列為空
	if ((*plist)->next == NULL)
	{
		free(*plist);
		*plist = NULL;
	}
	else
	{
		SListNode* ptail = *plist;
		while (ptail->next->next)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}	
}

(2) 複雜度分析

  • 時間複雜度:需要遍歷整個連結串列,時間複雜度為O(N)。
  • 空間複雜度:不需要格外的空間消耗,空間複雜度為O(1)。

5.6 單連結串列的查詢

如果找到就返回這個節點的地址,否則就返回空指標NULL

(1) 程式碼實現

SListNode* SListSearch(SListNode* plist, SLTDataType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

(2) 時間複雜度

  • 時間複雜度:可能需要遍歷整個連結串列,時間複雜度為O(N)。
  • 空間複雜度:不需要格外的空間消耗,空間複雜度為O(1)。

5.6 單連結串列的修改

單連結串列的修改可以與查詢配套使用。

(1) 程式碼實現

void SListModify(SListNode*plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur == pos)
		{
			cur->data = x;
			break;
		}
		cur = cur->next;
	}
}

(2) 複雜度分析

  • 時間複雜度:可能需要遍歷整個連結串列,時間複雜度為O(N)。
  • 空間複雜度:不需要格外的空間消耗,空間複雜度為O(1)。

5.7 單連結串列任意位置的插入

(1) 程式碼實現

某個位置之前插入,當連結串列為空或者在頭節點插入時使用頭插。

void SListInsertF(SListNode** plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	if (*plist == pos || *plist == NULL)
	{
		SListPushFront(plist, x);//頭插
	}
	else
	{

		SListNode* newnode = SListCreatNode(x);
		SListNode* prev = *plist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

某個位置之後插入,當連結串列為空或者在尾節點插入時使用尾插。

void SListInsertB(SListNode** plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	if (*plist == NULL||pos->next==NULL)
	{
		SListPushBack(plist, x);//尾插
	}
	else
	{
		SListNode* tmp = pos->next;
		SListNode* newnode = SListCreatNode(x);
		pos->next = newnode;
		newnode->next = tmp;
	}
}

(2) 複雜度分析

  • 時間複雜度:無論向前插入還是向後插入都可能需要遍歷整個連結串列,所以時間複雜度為O(N)。
  • 空間複雜度:固定創造一個節點,空間複雜度為O(1)。

5.8 任意位置的刪除

任意位置的刪除需要提前儲存好前一個節點與後一個節點的位置。

(1) 程式碼實現

void SListErase(SListNode** plist, SListNode* pos)
{
	assert(plist);
	assert(*plist);
	assert(pos);
	if (*plist == pos)
	{
		SListPopFront(plist);//頭刪
	}
	SListNode* prev = *plist;
	while (prev->next!=pos)
	{
		prev = prev->next;
	}
	SListNode* tmp = pos->next;
	prev->next = tmp;
	free(pos);
	pos = NULL;
}

(2) 複雜度分析

  • 時間複雜度:可能需要遍歷整個連結串列,所以時間複雜度為O(N)。
  • 空間複雜度:固定創造一個節點,空間複雜度為O(1)。

5.9 銷燬連結串列

銷燬連結串列需要依次遍歷釋放節點。

(1) 程式碼實現

void SListDestroy(SListNode** plist)
{
	assert(plist);
	assert(*plist);
	SListNode* cur = *plist;
	while (cur)
	{
		SListNode* tmp = cur->next;
		free(cur);
		cur =tmp;
	}
	*plist = NULL;//防止野指標
}

(2) 複雜度分析

  • 時間複雜度:需要遍歷整個連結串列,所以時間複雜度為O(N)。
  • 空間複雜度:不需要格外的空間消耗,空間複雜度為O(1)。

6. 完整程式碼

(1) SList.h

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SListNode;
void PrintSList(SListNode* plist);//列印連結串列
void SListPushFront(SListNode** plist, SLTDataType x);//頭插
void SListPopFront(SListNode** plist);//頭刪
void SListPushBack(SListNode** plist,SLTDataType x);//尾插
void SListPopBack(SListNode** plist);//尾刪
SListNode* SListSearch(SListNode* plist, SLTDataType x);//查詢
void SListModify(SListNode* plist, SListNode* pos, SLTDataType x);//修改
void SListInsertF(SListNode** plist, SListNode* pos, SLTDataType x);//任意位置之前插入
void SListInsertB(SListNode** plist, SListNode* pos, SLTDataType x);//任意位置之後插入
void SListErase(SListNode** plist, SListNode* pos);//任意位置的刪除
void SListDestroy(SListNode** plist);//銷燬連結串列

(2) SList.c

#include"SList.h"
SListNode* SListCreatNode(SLTDataType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc fail:");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void PrintSList(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
void SListPushFront(SListNode** plist, SLTDataType x)
{
	assert(plist);
	SListNode* newnode = SListCreatNode(x);
	newnode->next = *plist;
	*plist = newnode;
}
void SListPopFront(SListNode** plist)
{
	assert(plist);
	assert(*plist);//防止連結串列為空
	SListNode* cur = (*plist)->next;
	free(*plist);
	*plist = cur;
}
void SListPushBack(SListNode** plist, SLTDataType x )
{
	assert(plist);
	if (*plist== NULL)
	{
		*plist = SListCreatNode(x);
	}
	else
	{
		SListNode* ptail = *plist;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = SListCreatNode(x);
	}
}
void SListPopBack(SListNode** plist)
{
	assert(plist);
	assert(*plist);//防止連結串列為空
	if ((*plist)->next == NULL)
	{
		free(*plist);
		*plist = NULL;
	}
	else
	{
		SListNode* ptail = *plist;
		while (ptail->next->next)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
	
}
SListNode* SListSearch(SListNode* plist, SLTDataType x)
{
	assert(plist);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
void SListModify(SListNode*plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	SListNode* cur = plist;
	while (cur)
	{
		if (cur == pos)
		{
			cur->data = x;
			break;
		}
		cur = cur->next;
	}
}
void SListInsertF(SListNode** plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	if (*plist == pos || *plist == NULL)
	{
		SListPushFront(plist, x);//頭插
	}
	else
	{

		SListNode* newnode = SListCreatNode(x);
		SListNode* prev = *plist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}
void SListInsertB(SListNode** plist, SListNode* pos, SLTDataType x)
{
	assert(plist);
	assert(pos);
	if (*plist == NULL||pos->next==NULL)
	{
		SListPushBack(plist, x);//尾插
	}
	else
	{
		SListNode* tmp = pos->next;
		SListNode* newnode = SListCreatNode(x);
		pos->next = newnode;
		newnode->next = tmp;
	}
}
void SListErase(SListNode** plist, SListNode* pos)
{
	assert(plist);
	assert(*plist);
	assert(pos);
	if (*plist == pos)
	{
		SListPopFront(plist);//頭刪
	}
	SListNode* prev = *plist;
	while (prev->next!=pos)
	{
		prev = prev->next;
	}
	SListNode* tmp = pos->next;
	prev->next = tmp;
	free(pos);
	pos = NULL;
}
void SListDestroy(SListNode** plist)
{
	assert(plist);
	assert(*plist);
	SListNode* cur = *plist;
	while (cur)
	{
		SListNode* tmp = cur->next;
		free(cur);
		cur =tmp;
	}
	*plist = NULL;//防止野指標
}

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章