資料結構 - 單連結串列 C++ 實現

Axyzstra發表於2022-03-29

單連結串列

單連結串列的定義

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    LNode *next;
} LNode, *LinkList;

此處 LNode 強調一個結點,*LinkList 強調一個單連結串列的頭指標,本例中只有頭指標使用 *LinkList ;

單連結串列的頭指標和頭節點

若單連結串列沒有頭節點,那麼單連結串列的頭指標則指向連結串列的第一個元素;若由頭節點,頭指標指向頭節點;例如頭指標為 L;如果連結串列為空,則有 L == NULL,若有頭節點,則有 L->next = NULL

注意此處的指向問題應當透徹理解指標的概念,指向理解為元素地址;此處的 L 為頭指標;在沒有頭節點時指向第一個元素,L 就是第一個元素的地址,若沒有元素,即沒有第一個元素,那麼 L == NULL;如果有頭節點,那麼 L 為頭節點的地址,因此 L->next 即為元素的第一個結點,故當連結串列為空時 L->next == NULL

本例中的單連結串列均為帶頭節點的單連結串列;

初始化一個單連結串列

初始化單連結串列的主要目的在於建立一個頭節點,並讓 L 指向頭節點;

L 為指向結點的指標,動態申請的記憶體大小為 sizeof(LNode),L 的型別為 LinKList 或者說 LNode* 因此需要轉化為 LinkList;程式碼如下:

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

建立一個單連結串列

頭插法

即將新元素插入到連結串列的第一個位置

void List_HeadInsert(LinkList &L) {
    for(int i = 1; i <= 10; i++) { //將 1 ~ 10 按頭插法插入單連結串列
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = L->next;
        L->next = p;
    }
    //按照頭插法的插入方式結果為倒序
    Show_List(L);
}

測試本段程式碼

#include<iostream>
#include<cstdlib>
using namespace std; 

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_HeadInsert(LinkList &L) {
    for(int i = 1; i <= 10; i++) { //將 1 ~ 10 按頭插法插入單連結串列
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = L->next;
        L->next = p;
    }
    //按照頭插法的插入方式結果為倒序
    Show_List(L);
}

int main() {
    LinkList L;
    ListInitite(L);
    List_HeadInsert(L);
    return 0;
}

執行結果

10 9 8 7 6 5 4 3 2 1

尾插法

即新的元素放在連結串列尾

使用尾插法,需要定義一個尾指標 r,尾指標始終指向連結串列的最後一個元素;剛開始為空連結串列,尾指標指向頭節點,即和 L 相等,此後每插入一個新的結點,新的結點成為新的尾結點,r 指向此結點;

void List_TailInsert(LinkList &L) {
    LNode* r = L;
    for(int i = 1; i <= 10; i++) {
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = r->next;
        r->next = p;
        r = p;
    }
}

測試:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_HeadInsert(LinkList &L) {
    for(int i = 1; i <= 10; i++) { //將 1 ~ 10 按頭插法插入單連結串列
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = L->next;
        L->next = p;
    }
    //按照頭插法的插入方式結果為倒序
    Show_List(L);
}

void List_TailInsert(LinkList &L) {
    LNode* r = L;
    for(int i = 1; i <= 10; i++) {
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = r->next;
        r->next = p;
        r = p;
    }
}

int main() {
    LinkList L;
    ListInitite(L);
    List_TailInsert(L);
    //按尾插法插入為順序 
    Show_List(L);
    return 0;
}

測試結果:

1 2 3 4 5 6 7 8 9 10

返回連結串列的長度

int Length(LinkList L) {
    LNode* p = L;
    int length = 0;
    while (p->next) {
        length++;
        p = p->next;
    }
    return length;
}

連結串列的查詢

按序號查詢結點的值

即查詢第 i 個結點的值,最終返回此結點

LNode* GetElem(LinkList L, int i) {
    if(i == 0) {
        return L;
    }
    if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
        return NULL;
    }
    LNode* p = L;
    int now = 0;
    while(p && now < i) {
        p = p->next;
        now++;
    }
    return p;
}

測試:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_TailInsert(LinkList &L) {
    LNode* r = L;
    for(int i = 1; i <= 10; i++) {
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = r->next;
        r->next = p;
        r = p;
    }
}

int Length(LinkList L) {
    LNode* p = L;
    int length = 0;
    while (p->next) {
        length++;
        p = p->next;
    }
    return length;
}


LNode* GetElem(LinkList L, int i) {
    if(i == 0) {
        return L;
    }
    if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
        return NULL;
    }
    LNode* p = L;
    int now = 0;
    while(p && now < i) {
        p = p->next;
        now++;
    }
    return p;
}


int main() {
    LinkList L;
    ListInitite(L);
    List_TailInsert(L);
    LNode* ip = GetElem(L, 5);
    Show_List(L);
    if(ip) {
        printf("\n%d", ip->data);
    }
    return 0;
}

測試結果:

1 2 3 4 5 6 7 8 9 10
5

按值查詢結點

LNode* LocateElem(LinkList L, ElemType e) {
    LNode* p = L->next;
    while(p && p->data != e) {
        p = p->next;
    }
    return p;
}

插入結點

在連結串列的第 i 個位置插入元素 e

插入新的元素後共有 len + 1 個元素,插入位置也必須在 [1, len + 1],因此插入位置必須在這個範圍內;首先獲得第 i - 1 個結點,然後進行操作;

bool ListInsert(LinkList &L, int i, ElemType e) {
    if(i < 1 || i > Length(L) + 1) {
        printf("插入位置錯誤\n");
        return false;
    }
    LNode *pre, *s;
    s->data = e;
    pre = GetElem(L, i - 1);
    s->next = pre->next;
    pre->next = s;
    return true;
}

測試程式碼:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
	ElemType data;
	struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;
}

void Show_List(LinkList L) {
	LNode* p = L->next;
	while (p) {
		printf("%d ", p->data);
		p = p->next;
	}
}

void List_HeadInsert(LinkList &L) {
	for(int i = 1; i <= 10; i++) { //將 1 ~ 10 按頭插法插入單連結串列
		LNode* p = (LNode*)malloc(sizeof(LNode));
		p->data = i;
		p->next = L->next;
		L->next = p;
	}
}


int Length(LinkList L) {
	LNode* p = L->next;
	int length = 0;
	while(p) {
		length++;
		p = p->next;
	}
	return length;
}


LNode* GetElem(LinkList L, int i) {
	if(i == 0) {
		return L;
	}
	if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
		return NULL;
	}
	LNode* p = L;
	int now = 0;
	while(p && now < i) {
		p = p->next;
		now++;
	}
	return p;
}


bool ListInsert(LinkList &L, int i, ElemType e) {
    if(i < 1 || i > Length(L) + 1) {
        printf("插入位置錯誤\n");
        return false;
    }
    LNode *pre = GetElem(L, i - 1);
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = pre->next;
    pre->next = s;
    return true;
}

int main() {
	LinkList L;
	ListInitite(L);
	List_HeadInsert(L);
	ListInsert(L, 5, 100);
	Show_List(L);
	return 0;
}

測試結果:

10 9 8 7 100 6 5 4 3 2 1

前插和後插

前插即在一個已知結點的前面插入新的結點,後插即在一個已知結點的後面插入新的結點;

上面的插入函式即在結點的後面插入新的結點,首先需要得到第 i - 1 個結點,然後再此結點後面插入新的結點,即為後插;

前插操作也是類似,在某個結點的前面插入結點,首先獲取到此結點的前一個結點,然後在前一個結點後面插入新的結點;但這種插入方式必須首先獲取到已知結點的前一個結點,查詢過程必須遍歷當前結點之前的所有元素才能找到前一個結點;時間複雜度為 O(n),採用另一種方式可以巧妙的將複雜度降低到 O(1);方法為在已知結點的後面插入新的結點,然後交換新節點與已知結點的值,就實現了相同的目的;

void FrontInsert(LNode* node, ElemType e) {
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = node->next;
    node->next = s;
    ElemType temp = node->data;
    node->data = s->data;
    s->data = temp;
}

2~5 行操作為將新的結點插入到已知結點的後面,6~8 行操作為交換兩個結點內的值;

測試:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_HeadInsert(LinkList &L) {
    for(int i = 1; i <= 10; i++) { //將 1 ~ 10 按頭插法插入單連結串列
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = L->next;
        L->next = p;
    }
}


int Length(LinkList L) {
    LNode* p = L->next;
    int length = 0;
    while(p) {
        length++;
        p = p->next;
    }
    return length;
}


LNode* GetElem(LinkList L, int i) {
    if(i == 0) {
        return L;
    }
    if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
        return NULL;
    }
    LNode *p = L;
    int now = 0;
    while(p && now < i) {
        p = p->next;
        now++;
    }
    return p;
}


void FrontInsert(LNode* &node, ElemType e) {
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = node->next;
    node->next = s;
    ElemType temp = node->data;
    node->data = s->data;
    s->data = temp;
}


int main() {
    LinkList L;
    ListInitite(L);
    List_HeadInsert(L);
    Show_List(L);
    LNode *node = GetElem(L, 5);
    FrontInsert(node, 50);
    printf("\n");
    Show_List(L);
    return 0;
}

測試結果:

10 9 8 7 6 5 4 3 2 1
10 9 8 7 50 6 5 4 3 2 1

刪除結點操作

刪除連結串列位置為 i 的結點,並將刪除的結點存放在 node 中

bool ListDelete(LinkList &L, int i, LNode* &node) {
    if(i < 1 || i > Length(L)) {
        printf("刪除位置錯誤");
        return false;
    }
    LNode *p = GetElem(L, i - 1);
    LNode *q = p->next;
    p->next = q->next;
    node = q;
    free(q);
    return true;
}

上述程式碼有錯,free(void* p) 函式的作用是回收 動態分配給 p 的空間,不論有多少指標指向 p 所指向的空間,因此將對於 node = q,在 free(q) 以後 node 所指向的空間也被回收了,因此此處最好不返回結點,返回結點中的值;修正後的程式碼如下:

bool ListDelete(LinkList &L, int i, ElemType &del) {
    if(i < 1 || i > Length(L)) {
        printf("刪除位置錯誤");
        return false;
    }
    LNode *p = GetElem(L, i - 1);
    LNode *q = p->next;
    p->next = q->next;
    del = q->data;
    free(q);
    return true;
}

測試:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_TailInsert(LinkList &L) {
    LNode* r = L;
    for(int i = 1; i <= 10; i++) {
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = r->next;
        r->next = p;
        r = p;
    }
}

int Length(LinkList L) {
    LNode* p = L->next;
    int length = 0;
    while(p) {
        length++;
        p = p->next;
    }
    return length;
}


LNode* GetElem(LinkList L, int i) {
    if(i == 0) {
        return L;
    }
    if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
        return NULL;
    }
    LNode *p = L;
    int now = 0;
    while(p && now < i) {
        p = p->next;
        now++;
    }
    return p;
}


bool ListDelete(LinkList &L, int i, ElemType &del) {
    if(i < 1 || i > Length(L)) {
        printf("刪除位置錯誤");
        return false;
    }
    LNode *p = GetElem(L, i - 1);
    LNode *q = p->next;
    p->next = q->next;
    del = q->data;
    free(q);
    return true;
}

int main() {
    LinkList L;
    ListInitite(L);
    List_TailInsert(L);
    Show_List(L);
    ElemType del;
    ListDelete(L, 7, del);
    printf("\n");
    Show_List(L);
    printf("\n刪除的元素為:%d", del);
    return 0;
}

結果:

1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 8 9 10
刪除的元素為:7

此處刪除的實現依然為後刪,即找到將要刪除結點的前一個結點進行刪除;即給定一個已知結點需要對其進行刪除,首先應該找到其前驅節點才能進行刪除;和前插法類似,也有減少其複雜度的方法,即首先交換待刪除結點後其後繼節點的值,然後刪除其後繼節點;實現方式和前插法類似:

void Del(LinkList &L, LNode* &p) {
    LNode* q = p->next;
    ElemType temp = q->data;
    q->data = p->data;
    p->data = temp;
    p->next = q->next;
    free(q);
}

當然此時對於極端情況,即要刪除的元素為最後一個元素時不適用;

測試:

#include<iostream>
#include<cstdlib>
using namespace std;

typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *next;
} LNode, *LinkList;

void ListInitite(LinkList &L) {
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
}

void Show_List(LinkList L) {
    LNode* p = L->next;
    while (p)
    {
        printf("%d ", p->data);
        p = p->next;
    }
}

void List_TailInsert(LinkList &L) {
    LNode* r = L;
    for(int i = 1; i <= 10; i++) {
        LNode* p = (LNode*)malloc(sizeof(LNode));
        p->data = i;
        p->next = r->next;
        r->next = p;
        r = p;
    }
}

int Length(LinkList L) {
    LNode* p = L->next;
    int length = 0;
    while(p) {
        length++;
        p = p->next;
    }
    return length;
}


LNode* GetElem(LinkList L, int i) {
    if(i == 0) {
        return L;
    }
    if(i < 1 || i > Length(L)) {   //若超出連結串列範圍
        return NULL;
    }
    LNode *p = L;
    int now = 0;
    while(p && now < i) {
        p = p->next;
        now++;
    }
    return p;
}



void Del(LinkList &L, LNode* &p) {
    LNode* q = p->next;
    ElemType temp = q->data;
    q->data = p->data;
    p->data = temp;
    p->next = q->next;
    free(q);
}

int main() {
    LinkList L;
    ListInitite(L);
    List_TailInsert(L);
    Show_List(L);
	LNode *p = GetElem(L, 4);
	Del(L, p);
	printf("\n");
	Show_List(L);
    return 0;
}

結果:

1 2 3 4 5 6 7 8 9 10
1 2 3 5 6 7 8 9 10

單連結串列的銷燬

void Destory(LinkList &L) {
    LNode* p = L;
    LNode* q = L;
    while (q)
    {
        p = q;
        q = q->next;
        free(p);
    }
   free(L);  
   L=NULL;
} 

相關文章