博客 / 詳情

返回

數據結構入門:順序表/鏈表/棧/隊列/堆(原理+實現)

一、開篇引入

作為計算機專業的學生,順序表、鏈表、棧、隊列、堆是《數據結構》課程的核心基礎,也是算法刷題、工程開發的"必備工具",但新手剛接觸時,常容易混淆各結構的特性,也難以用C語言實現,本文就從概念原理出發,用C語言完成這些數據結構的實現,手把手幫助新手徹底搞懂這些核心結構。

二、數據結構的實現

1.順序表

順序表是線性表的一種,滿足邏輯結構和物理結構雙線性
邏輯結構:數組元素之間呈"一對一"的先後順序,是邏輯上的線性結構,可能與實際結構並不相同
物理結構:底層基於數組實現,數組的內存空間是連續且不可分割,因此數據在物理存儲上也連續

1.1.順序表的結構:

點擊查看代碼
typedef struct SeqList {
	SLdatatype* data;//數組的指針
	int size;//有效數據的個數
	int capacity;//數組的實際空間
}SL;

1.2.順序表的接口:

點擊查看代碼
//初始化順序表
void InitSL(SL* ps);
//銷燬順序表
void DestroySL(SL* ps);
//申請空間
void SLbuy(SL* ps, SLdatatype x);
//打印
void SLprint(SL ps);
//頭插
void SLpushfront(SL* ps, SLdatatype x);
//尾插
void SLpushback(SL* ps, SLdatatype x);
//在指定位置之前插入數據
void SLInsert(SL* ps, int pos, SLdatatype x);
//尾刪
void SLpopback(SL* ps);
//頭刪
void SLpopfront(SL* ps);
//刪除指定位置的數據
void SLErase(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLdatatype x);

1.3.順序表的實現:

點擊查看代碼
//初始化
void InitSL(SL* ps) {
	ps->data = NULL;
	ps->capacity = ps->size = 0;
}
//銷燬
void DestroySL(SL* ps) {
	if (ps->data) {
		free(ps->data);
	}
	ps->data = NULL;
	ps->capacity = ps->size = 0;
}
//打印順表表中的元素
void SLprint(SL s) {
	for (int i = 0; i < s.size; i++) {
		printf("%d", s.data[i]);
	}
	printf("\n");
}
//申請空間
void SLbuy(SL* ps) {
    //如果不這樣capacity = 0 時,capacity就永遠為0
	int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
    //這裏是relloc(擴容),如果是malloc就是重新開闢了一塊空間,那麼原來的數據就丟失了
	SLdatatype* tmp = (SLdatatype*)realloc(ps->data, newcapacity * sizeof(SLdatatype));
	if (tmp == NULL) {
		perror("SLpushback():realloc Fail");
		exit(1);
	}
    //這裏申請完空間後注意重新賦值capacity
	ps->capacity = newcapacity;
	ps->data = tmp;
}
//尾插
void SLpushback(SL* ps,SLdatatype x) {
	assert(ps);
	if (ps->capacity == ps->size) {
		SLbuy(ps);
	}
    //push完size++
	ps->data[ps->size++] = x;
}
//頭插
void SLpushfront(SL* ps, SLdatatype x) {
	assert(ps);
	if (ps->capacity == ps->size) {
		SLbuy(ps);
	}
	for (int i = ps->size; i > 0; i--) {
		ps->data[i] = ps->data[i - 1];
	}
    //push完size++
	ps->data[0] = x;
	ps->size++;
}
//任意位置插入
void SLInsert(SL* ps, int pos, SLdatatype x) {
	assert(ps);
    //防止越界的問題
	assert(pos >= 0 && pos <= ps->size);
	if (ps->capacity == ps->size) {
		SLbuy(ps);
	}
	for (int i = ps->size; i > pos; i--) {
		ps->data[i] = ps->data[i - 1];
	}
	ps->data[pos] = x;
	ps->size++;
}
//尾刪
void SLpopback(SL* ps) {
	assert(ps);
	assert(ps->size);
    //刪完後size--
	--(ps->size);
}
//頭刪
void SLpopfront(SL* ps) {
	assert(ps);
	assert(ps->size);
	for (int i = 0; i < ps->size - 1; i++) {
		ps->data[i] = ps->data[i + 1];
	}
	--(ps->size);
}
//任意位置刪
void SLErase(SL* ps, int pos) {
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i<ps->size-1; i++) {
		ps->data[i] = ps->data[i + 1];
	}
	ps->size--;
}
int SLFind(SL* ps, SLdatatype x) {
	assert(ps);
	for (int i = 0; i < ps->size; i++) {
		if (ps->data[i] == x) {
			return i;
			break;
		}
	}
    //只要返回無效的下標就行
	return -1;
}

2.鏈表

邏輯結構線性,物理結構離散
邏輯結構:數據元素之間通過指針建立"一對一"的先後順序,邏輯上呈線性。
物理結構:節點的內存地址並不連續,不能連續訪問

2.1.鏈表的結構:

點擊查看代碼
typedef int SlDatatype;
typedef struct Slistnode {
	SlDatatype data;
	struct Slistnode* next;
}Slnode;

2.2.鏈表的接口:

點擊查看代碼
//鏈表的申請空間
Slnode* SlistBuy(SlDatatype x);
//鏈表的打印
void SlistPrint(Slnode* phead);
//鏈表的尾插
void SlistPushback(Slnode** phead, SlDatatype data);
//鏈表的頭插
void SlistPushhead(Slnode** pphead, SlDatatype data);
//鏈表的尾刪
void Slistdeleteback(Slnode** pphead);
//鏈表的頭刪
void Slistdeletehead(Slnode** pphead);
//鏈表的查找
Slnode* SlistFind(Slnode** pphead, SlDatatype x);
//鏈表指定位置之前插入節點
void Slistinert(Slnode** pphead, Slnode* pos, SlDatatype x);
//鏈表指定位置之後插入節點
void Slistinertback(Slnode* pos, SlDatatype x);
//鏈表指定位置刪除節點
void SlistErase(Slnode** pphead, Slnode* pos);
//鏈表指定位置之後刪除節點
void SlistsEraseBack(Slnode* pos);

2.3.鏈表的實現:

點擊查看代碼
//申請空間
Slnode* SlistBuy(SlDatatype x) {
	Slnode* newnode = (Slnode*)malloc(sizeof(Slnode));
	if (newnode == NULL) {
		perror("malloc fail");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
//鏈表的打印
void SlistPrint(Slnode* phead) {
	Slnode* tmp = phead;
	while (tmp) {
		printf("%d->", tmp->data);
		tmp=tmp->next;
	}
	printf("NULL\n");

}
//鏈表的尾插->傳二級指針,因為只要涉及改變參數的都要地址拷貝
void SlistPushback(Slnode** pphead, SlDatatype data) {
	//尋找尾部
	assert(pphead);
	Slnode* tmp = *pphead;
	Slnode*newnode = SlistBuy(data);
	if (tmp== NULL) {
		*pphead = newnode;
	}
	else {
		while (tmp->next) {
			tmp=tmp->next;
		}
		tmp->next = newnode;
		newnode->data = data;
		newnode->next = NULL;
	}
}
//鏈表的頭插
void SlistPushhead(Slnode** pphead, SlDatatype data) {
	assert(pphead);
	Slnode* tmp = *pphead;
	Slnode* newnode = SlistBuy(data);
	if (tmp == NULL) {
		*pphead= newnode;
	}
	else {
		*pphead = newnode;
		newnode->data = data;
		newnode->next = tmp;
	}
}
//鏈表的尾刪
void Slistdeleteback(Slnode** pphead) {
	assert(pphead && *pphead);
	Slnode* tmp = *pphead;
	Slnode* prev = *pphead;
	if (tmp -> next ==NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		while (tmp->next) {
			prev = tmp;
			tmp = tmp->next;
		}
	}
	free(tmp);
	tmp = NULL;
	prev->next = NULL;
}
void Slistdeletehead(Slnode** pphead) {
	assert(pphead && *pphead);
	Slnode* cmp = *pphead;
	*pphead = cmp->next;
	free(cmp);
	cmp = NULL;
}
void Slistinert(Slnode** pphead, Slnode* pos, SlDatatype x) {
	assert(pphead&&pos&&*pphead);
	Slnode* newnode = SlistBuy(x);
	Slnode* prev = *pphead;
	Slnode* pur = *pphead;
	if (pos == *pphead) {
		SlistPushhead(pphead, x);
	}
	else {
		while (pur != pos) {
			prev = pur;
			pur = pur->next;
		}
		prev->next = newnode;
		newnode->next = pur;
	}
}
Slnode* SlistFind(Slnode** pphead, SlDatatype x) {
	assert(pphead && *pphead);
	int Flag = 0;
	Slnode* tmp = *pphead;
	while (tmp) {
		if (tmp->data == x) {
			Flag = 1;
			return tmp;
		}
		tmp = tmp->next;
	}
	if (Flag == 0) {
		return NULL;
	}
}
void Slistinertback(Slnode* pos, SlDatatype x) {
	assert(pos);
	Slnode* newnode = SlistBuy(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
void SlistErase(Slnode** pphead, Slnode* pos) {
	assert(pphead && pos);
	Slnode* prev = *pphead;
	Slnode* pur = *pphead;
	if (pos == *pphead) {
		Slnode* toDel = *pphead;
		*pphead = toDel->next;
		free(toDel);
		return;
	}

	while (pur != pos) {
		prev = pur;
		pur = pur->next;
	}
	prev->next = pur->next;
	free(pur);
	pur = NULL;
}
void SlistsEraseBack(Slnode* pos) {
	assert(pos&&pos->next);
	Slnode* tmp = pos->next;
	pos->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

簡單總結一下鏈表和順序表的不同點:

屏幕截圖 2026-01-12 222859

緩存:由於CPU的運行速率超快,與內存不同頻,需要把內存中的內容先轉到寄存器中,而寄存器由於空間有限,所以要先轉到緩存器中,然後等寄存器運行結束後,再將緩存器中的內容轉到寄存器中。(簡單的理解)
順序表的緩存利用率高:數組由於空間連續,緩存器一次取順序表的內容就可以一大片一大片的取,緩存命中率就高,而鏈表由於空間不一定連續,所以就只能一次一次的取,效率就慢,緩存命中率就低。

3.棧

棧是一種後進先出的線性表,僅允許在棧頂進行插入和刪除

3.1.棧的實現選擇:如果是用鏈表實現,我們就需要頻繁的找尾,而每次找尾的時間複雜度為O(N),並且再刪除尾部的時候還要保存上一節點,操作繁瑣。所以我們選擇了更易操作的數組實現。

3.2.棧的結構:

點擊查看代碼
typedef int STdatatype;
typedef struct Stack {
	STdatatype* a;
	int top;
	int capacity;
}ST;

3.3.棧的接口:

點擊查看代碼
//棧的初始化
void STinit(ST* pst); 
//棧的銷燬
void STdestroy(ST* pst);
//入棧
void STpush(ST* pst,STdatatype x);
//出棧
void STpop(ST* pst);
//取棧頂數據
STdatatype STtop(ST* pst);
//判空
int STempty(ST* pst);
//獲取數據個數
int STsize(ST* pst);

3.4.棧的實現:

點擊查看代碼
void STinit(ST* pst) {
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STdestroy(ST* pst) {
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STpush(ST* pst,STdatatype x) {
	assert(pst);
	if (pst->capacity == pst->top) {
		int newcapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
		STdatatype* tmp = (STdatatype*)realloc(pst->a,newcapacity*sizeof(STdatatype));
		if (tmp == NULL) {
			perror("STPush():realloc():Fail");
			return;
		}
		pst->capacity = newcapacity;
		pst->a = tmp;
	}
	pst->a[pst->top++] = x;
}
void STpop(ST* pst) {
	assert(pst);
	assert(pst->top);
	pst->top--;
}
STdatatype STtop(ST* pst) {
	assert(pst);
	assert(pst->top);
	return pst->a[pst->top - 1];
}
int STempty(ST* pst) {
	assert(pst);
	return pst->top == 0;
}
int STsize(ST* pst) {
	assert(pst);
	int size = pst->top;
	return size;
}

利用棧可以解決經典算法題(https://leetcode.cn/problems/valid-parentheses/)

思路:
1.若遇到左括號就入棧
2.若遇到右括號就嘗試與棧頂的左括號匹配

代碼實現:

點擊查看代碼
bool isValid(char* s) {
    ST st;
    STinit(&st);
    //遍歷字符串
    while(*s){
        //如果是左括號就入棧
        if(*s == '(' || *s == '[' || *s == '{'){
            STpush(&st,*s);
        }
        //如果是右括號就嘗試與左括號匹配
        else{
            if(STempty(&st)){
                STdestroy(&st);
                return false;
            }
            char top = STtop(&st);
            STpop(&st);
            if((top == '(' && *s != ')') ||
            (top == '[' && *s != ']') || 
            (top == '{' && *s != '}')){
                STdestroy(&st);
                return false;
            }
        }
        ++s;
    }
    bool ret = STempty(&st);
    STdestroy(&st);
    return ret;   
}

4.隊列

隊列是一種先進先出的線性表,在隊頭刪除,在隊尾插入

4.1.隊列的應用:醫院抽號機,還有社交平台上的好友推薦

4.2.隊列的實現選擇:如果用數組實現,每次刪除的是首元素,這樣首元素後面的元素就得向前移動,時間複雜度為O(N),所以我們用鏈表更易實現

4.3.隊列的結構:特殊點:額外用一個結構體封裝了它的頭節點,尾節點,以及大小,這樣傳參數的時候就能只傳一個參數了

點擊查看代碼
typedef int Qdatatype;
typedef struct QueueNode {
	struct QueueNode* next;
	Qdatatype val;
}QNode;

typedef struct Queue {
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

4.4.隊列的接口:

點擊查看代碼
//隊列的初始化
void QueueInit(Queue* pq);
//隊列的銷燬
void QueueDestroy(Queue* pq);
//隊頭數據的刪除
void QueuePop(Queue* pq);
//隊尾插入數據
void QueuePush(Queue* pq, Qdatatype x);
//獲取隊頭的數據
Qdatatype QueueFront(Queue* pq);
//獲取隊尾的數據
Qdatatype QueueBack(Queue* pq);
//隊列的長度
int QueueSize(Queue* pq);
//隊列的判空
bool QueueEmpty(Queue* pq);

4.5.隊列的實現:

點擊查看代碼
void STinit(ST* pst) {
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STdestroy(ST* pst) {
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}
void STpush(ST* pst,STdatatype x) {
	assert(pst);
	if (pst->capacity == pst->top) {
		int newcapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
		STdatatype* tmp = (STdatatype*)realloc(pst->a,newcapacity*sizeof(STdatatype));
		if (tmp == NULL) {
			perror("STPush():realloc():Fail");
			return;
		}
		pst->capacity = newcapacity;
		pst->a = tmp;
	}
	pst->a[pst->top++] = x;
}
void STpop(ST* pst) {
	assert(pst);
	assert(pst->top);
	pst->top--;
}
STdatatype STtop(ST* pst) {
	assert(pst);
	assert(pst->top);
	return pst->a[pst->top - 1];
}
int STempty(ST* pst) {
	assert(pst);
	return pst->top == 0;
}
int STsize(ST* pst) {
	assert(pst);
	int size = pst->top;
	return size;
}

學了隊列可以嘗試一下這道題(https://leetcode.cn/problems/design-circular-queue/description/)

總結一下棧和隊列的區別

棧和隊列的核心區別就是:一個後進先出,一個先進先出
用兩個隊列實現棧(https://leetcode.cn/problems/implement-stack-using-queues/)
用兩個棧實現隊列(https://leetcode.cn/problems/implement-queue-using-stacks/)
思路如下:

屏幕截圖 2026-01-12 224034

5.堆

特殊的完全二叉樹,分為大堆和小堆
大堆:父節點>=子節點
小堆:父節點<=子節點

5.1.堆的存儲特性:

物理上基於數組存儲,邏輯上表現為完全二叉樹
對於一個下標為i的節點:
若存在子節點
左孩子的節點下標:2i + 1
右孩子的節點下標:2
i + 2
父節點下標:(i - 1)/2

5.2.堆的結構:

點擊查看代碼
typedef int Hpdatatype;

//堆(完全二叉樹)的底層還是一個數組
typedef struct Heap {
	Hpdatatype* a;
	int size;
	int capacity;
}Heap;

5.3.堆的接口:

點擊查看代碼
//交換函數
void Swap(Hpdatatype* ph1, Hpdatatype* ph2);
//向上調整法
void AdjustUp(Hpdatatype* a, int child);
//向下調整法
void AdjustDown(Hpdatatype* a, int size, int parent);
//堆的初始化
void HeapInit(Heap* php);
//堆的銷燬
void HeapDestroy(Heap* php);
//堆的插入數據
void HeapPush(Heap* php, Hpdatatype x);
//堆的刪除堆頂數據
void HeapPop(Heap*php);
//堆的獲取堆頂數據
Hpdatatype HeapTop(Heap* php);
//堆的判空
bool HeapEmpty(Heap* php);

5.4.堆的實現:

點擊查看代碼
void HeapInit(Heap* php) {
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

void HeapDestroy(Heap* php) {
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

void Swap(Hpdatatype* hp1, Hpdatatype* hp2) {
	assert(hp1 && hp2);
	Hpdatatype tmp = *hp1;
	*hp1 = *hp2;
	*hp2 = tmp;
}

void HeapPush(Heap* php, Hpdatatype x) {
	//實現小堆
	int child = php->size;
	int parent = (child - 1) / 2;
	//判斷空間夠不夠
	if (php->capacity == php->size) {
		int newcapacity = php->capacity == 0 ? 4 : 2 * (php->capacity);
		Hpdatatype* tmp = (Hpdatatype*)realloc(php->a,newcapacity * sizeof(Hpdatatype));
		if (tmp == NULL) {
			perror("HeapPush():realloc():fail");
			return;
		}
		php->capacity = newcapacity;//變化過後capacity沒有改變
		php->a = tmp;
	}
	php->a[child] = x;
	php->size++;
	//當child = 0 的時候停止
	AdjustUp(php->a, child);
}

void HeapPop(Heap* php) {
	//每次pop的意義是把次小的數推上堆頂
	assert(php);
	assert(php->size);
	//首先先將堆頂的數據與堆尾的數據交換
	Swap(&php->a[0], &php->a[php->size - 1]);
	--php->size;
	//再使用向下查找算法
	AdjustDown(php->a, php->size, 0);
}

Hpdatatype HeapTop(Heap* php) {
	assert(php);
	assert(php->size);
	return php->a[0];
}

bool HeapEmpty(Heap* php) {
	assert(php);
	return php->size == 0;
}

void AdjustUp(Hpdatatype* a, int child){
	//向上和向下調整法的時間複雜度都為O(N)
	int parent = (child - 1) / 2;
	while (child > 0) {
		//如果不滿足就重複交換child 和 parent 的值
		if (a[child] < a[parent]) {
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//當child > parent 的時候停止
		else {
			break;
		}
	}
}

void AdjustDown(Hpdatatype * a, int size, int parent) {
	int lesschild = 2 * parent + 1;
	//結束條件是到達葉子
	while (lesschild < size) {
		//假設法找到字節點中較小的那個
		if (lesschild + 1<size && a[lesschild] > a[lesschild + 1]) {
			lesschild++;
		}
		//將較小的子節點與父節點相比較
		if (a[lesschild] < a[parent]) {
			Swap(&a[lesschild], &a[parent]);
			parent = lesschild;
			lesschild = 2 * parent + 1;
		}
		else {
			break;
		}
	}
}

5.5.堆排序:

點擊查看代碼
void HeapSort(Hpdatatype* a, int size) {
	int parent = (size - 2) / 2;
	while (parent--) {
		AdjustDown(a, size, parent);//已經調成小堆的形式
	}
	while (size) {
		Swap(&a[0], &a[size - 1]);
		size--;
		AdjustDown(a, size, 0);
	}
}

三、結語

數據結構是計算機學科的基石,掌握順序表、鏈表、棧、隊列、堆的核心特性和實現,是後續學習樹、圖、排序算法的基礎。建議結合LeetCode算法題反覆練習,將理論轉化為實戰能力。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.