☰
深挖数据结构:线性表(1)
2026/10/15 11:40:31 网站建设 项目流程

目录

1.线性表

2. 顺序表

顺序表的结构定义(静态版)

顺序表的结构定义(动态版)

初始化

销毁

检查容量并扩容

尾插

头插

指定位置插入(在pos处插入x)

删除指定位置的元素

查找(返回下标,找不到返回 -1)

打印

完整测试示例

3. 链表

3.1单链表(带哨兵位)

结构定义

初始化

销毁

查找

求长度

打印

插入(头插+中间插+尾插)

删除(头删+中间删+尾删)

测试代码:

3.2单链表(不带哨兵位)

插入

删除

其他:

3.3 经典题目练习(不带哨兵位)

3.3.1链表的中间结点

3.3.2返回倒数第k个结点

3.3.3移除链表所有目标元素

3.3.4反转链表

3.3.5合并两个有序链表


本文概括:

1、讲述了线性表中,顺序表与单链表的基本概念、语法、代码。

2、单链表中,带哨兵位和不带哨兵位单链表的对比与优劣。

3、单链表经典题目练习。


1.线性表

含义:线性表是n个元素的有限序列;“线性”表示元素之间是“一对一”的关系,像排队一样,每个元素最多有一个前驱和一个后继。

线性表是逻辑结构,物理上通常用两种方式实现:

实现方式存储结构特点
顺序表(数组)顺序存储内存连续,支持随机访问,插入删除麻烦
链表(指针)链式存储内存不连续,访问慢,插入删除方便

顺序表示例(数组):

int arr[5] = {1,2,3,4,5};

链表示例(单链表):

struct Node{ int data; struct Node* next; };

注意:

1.线性表 ≠ 数组。线性表是逻辑结构,数组是存储方式之一。

2.线性表 ≠ 链表。链表只是实现线性表的一种方式。


2. 顺序表

概念:顺序表是线性表的一种物理存储方式;用一段连续的内存空间(数组)按顺序存储元素。

顺序表的结构定义(静态版)

#include<stdlib.h> typedef int Datatype; #define MXA 10; typedef struct Seqlist{ SLDataType arr[MAX]; //存储数据的静态数组 int size; //存储元素个数 }SL;

上述是静态数组,一般应用较少,主要使用下面讲的动态数组:


顺序表的结构定义(动态版)

#include<stdlib.h> typedef int SLDatatype; typedef struct SeqList{ SLDataType* a; // 指向动态开辟的数组 int size; // 实际存放的元素个数 int capacity; // 当前空间最大容量 }t_t;

初始化

void SLInit(t_t* ps) { ps->a = NULL; ps->size = 0; ps->capacity = 0; }

销毁

void SLDestory(t_t* ps) { free(ps->a); ps->a = NULL; ps->size = ps->capacity = 0; }

检查容量并扩容

void SLCheckCapacity(t_t* ps) { if(ps->size == ps->capacity) { int new_capacity = ps->capacity == 0 ? 4 : ps->capacity*2; SLDataType* tmp = (SLDataType*)realloc(ps->a,new_capacity * szieof(SLDataType)); if(tmp == NULL) { perror("realloc"); exit(1); } ps->a = tmp; ps->capacity = new_capacity; }

尾插

void SLPushBack(t_t* ps , SLDataType x) { SLCheckCapacity(ps); ps->a[ps->size++] = x; }

头插

void SLPushBack(t_t* ps , SLDataType x) { SLCheckCapacity(ps); //从后往前移,腾出一个位置 for(int i = ps->size;i > 0;i--) { ps->a[i] = ps->a[i - 1]; } ps->a[0] = x; ps->size++; }

指定位置插入(在pos处插入x)

void SLInsert(t_t* ps , int pos , SLDataType x) { if(pos < 0 || pos > ps->size) { printf("插入位置非法\n"); return; } SLCheckCapacity(ps); for(int i = ps->size;i > pos;i--) { ps->a[i] = ps->a[i - 1]; } ps->a[pos] = x; ps->size++; }

删除指定位置的元素

void SLErase(t_t* ps , int pos) { if(pos < 0 || pos >= ps->size) { printf("删除位置非法\n"); return; } for(int i = pos ; i < ps->size - 1;i++) { ps->a[i] = ps->a[i + 1]; } ps->size--; }

查找(返回下标,找不到返回 -1)

int SLFind(t_t* ps , SLDataType x) { for(int i = 0 ; i < ps->size ; i++) { if(ps->a[i] == x) return i; } return -1; }

打印

void SLDPrint(t_t* ps) { for(int i = 0 ; i < ps->size ; i++) { printf("%d ",ps->a[i]); } printf("\n"); }

完整测试示例

int main() { t_t s; SLInit(&s); SLPushBack(&s, 1); SLPushBack(&s, 2); SLPushFront(&s, 0); SLInsert(&s, 2, 99); SLPrint(&s); // 0 1 99 2 SLErase(&s, 1); SLPrint(&s); // 0 99 2 printf("find 99: %d\n", SLFind(&s, 99)); // 1 SLDestroy(&s); return 0; }

注意:

1. 顺序表就是数组?

数组是物理存储,顺序表是逻辑结构,用数组实现而已。

2. size 等于 capacity?

size 是实际元素个数,capacity 是容量, size <= capacity

3. 扩容直接用 ps->a = realloc(...)覆盖原指针?

必须使用临时指针接收,失败保留原指针,防止内存泄漏。


3. 链表

3.1单链表(带哨兵位)

含义:链表是一种物理上不连续、逻辑上连续的线性数据结构。它通过指针把一个个结点串起来,每个结点存数据,同时存指向下一个结点的地址。

结构定义

#include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int LDataType; typedef struct ListNode { LDataType* data; struct ListNode* next; }LNode;

两种单链表如图所示:有无哨兵位头结点

哨兵位头指针代码复杂度
有不会因插入删除而改变低,边界统一
无可能改变,需要二级指针或返回值高,头插/头删要特判

初始化

LNode* ListNode_Init(LDataType data) { LNode* Newnode = (LNode*)malloc(sizeof(LNode)); if (Newnode == NULL) { printf("内存开辟失败!!!\n"); exit(-1); } Newnode->data = data; Newnode->next = NULL; return Newnode; }

销毁

void ListNode_Destory(LNode* L) { assert(L); // 释放有效结点 LNode* cur = L->next; while (cur) { LNode* tmp = cur->next; free(cur); cur = tmp; } // 释放哨兵位 free(L); }

查找

LNode* ListNode_Find(LNode* L, LDataType val) { assert(L); LNode* cur = L->next; while (cur) { if (cur->data == val) return cur; cur = cur->next; } return NULL; }

求长度

// 求长度 int ListNode_Length(LNode* L) { assert(L); LNode* cur = L->next; int count = 0; while (cur) { count++; cur = cur->next; } return count; }

打印

void ListNode_Print(LNode* L) { assert(L); printf("头结点->"); LNode* cur = L->next; while (cur) { printf("%d->", cur->data); cur = cur->next; } printf("NULL"); printf("\n"); }

插入(头插+中间插+尾插)

// 在下标 i 的位置插入元素 x void ListNode_Insert(LNode* L, int i, LDataType x) { assert(L); assert(i >= 0); // 找 i-1 结点 LNode* cur = L; int j = -1; while (j < i - 1 && cur) { ++j; cur = cur->next; } // 插入位置不合法,结束 if (cur == NULL) return; LNode* New_Node = ListNode_Init(x); New_Node->next = cur->next; cur->next = New_Node; }

删除(头删+中间删+尾删)

// 删除下标为 i 的结点 void ListNode_Delete(LNode* L, int i) { assert(L); assert(i >= 0); // 找 i-1 结点 LNode* cur = L; int j = -1; while (j < i - 1 && cur) { ++j; cur = cur->next; } // 删除位置不合法,结束 if (cur == NULL || cur->next == NULL) return; LNode* tmp = cur->next; cur->next = cur->next->next; free(tmp); }

测试代码:

int main() { LNode* L = ListNode_Init(-1); ListNode_Insert(L, 0 , 0); ListNode_Insert(L, 1 , 1); ListNode_Insert(L, 2 , 2); ListNode_Insert(L, 3 , 3); ListNode_Insert(L, 4 , 4); printf("--创建单链表:--\n"); ListNode_Print(L); printf("--查找元素2:--\n"); LNode* Find = ListNode_Find(L, 2); printf("%d\n", Find->data); printf("--求链表长度:--\n"); printf("%d\n", ListNode_Length(L)); printf("--插入元素5:--\n"); ListNode_Insert(L, 5, 5); ListNode_Print(L); printf("--删除元素5:--\n"); ListNode_Delete(L , 5); ListNode_Print(L); printf("--销毁链表:--\n"); ListNode_Destory(L); printf("链表已销毁\n"); return 0; }

对应输出结果如下:

3.2单链表(不带哨兵位)

插入

由于插入需要找下标为 i - 1 的结点,当头插时,不带哨兵位的单链表的头结点没有前驱,因此头插需要单独判断,并且改变单链表的头结点。

// 插入 // 在下标 i 的位置插入元素 x void ListNode_Insert(LNode* L, int i, LDataType x) { assert(L); assert(i >= 0); LNode* cur = L; if (i == 0) { // 头插 LNode* New_Node = ListNode_Init(x); New_Node->next = cur; L = New_Node; }else { // 中间插 + 尾插 int j = 0; while (j < i - 1 && cur) { // 找 i-1 结点 ++j; cur = cur->next; } // 插入位置不合法,结束 if (cur == NULL) return; LNode* New_Node = ListNode_Init(x); New_Node->next = cur->next; cur->next = New_Node; } }

删除

删除的理由同上,删除时需要找到下标为 i - 1的结点,当头删时,不带哨兵位的单链表的头结点没有前驱,因此头删需要单独判断,并且改变单链表的头结点。

// 删除下标为 i 的结点 void ListNode_Delete(LNode* L, int i) { assert(L); assert(i >= 0); LNode* cur = L; if (i == 0) {// 头删 L = L->next; free(cur); return; }else {// 找 i-1 结点 int j = 0; while (j < i - 1 && cur) { ++j; cur = cur->next; } // 删除位置不合法,结束 if (cur == NULL || cur->next == NULL) return; LNode* tmp = cur->next; cur->next = cur->next->next; free(tmp); } }

其他:

初始化,销毁,查找,求长度,打印等,不带哨兵位的单链表,与带的单链表,代码整体思路与写法基本一致,不过要注意起始操作的结点可能不同,注意区分。


3.3 经典题目练习(不带哨兵位)

操作的单链表结构:

// 不带哨兵位的单链表 struct ListNode { int val; struct ListNode *next; };

3.3.1链表的中间结点

方法:快慢指针,快指针一次走两步,慢指针一次走一步。

struct ListNode* middleNode(struct ListNode* head) { // 排查空链表 if(head == NULL) { return NULL; } // 快慢指针 struct ListNode* Slow = head; struct ListNode* Fast = head; // Fast != NULL 和 Fast->next != NULL 的顺序不能对调 while(Fast != NULL && Fast->next != NULL) { Slow = Slow->next; Fast = Fast->next->next; } return Slow; }

3.3.2返回倒数第k个结点

方法:快慢指针,快的指针先走差距步,然后快慢指针再同时走。

int kthToLast(struct ListNode* head, int k) { // 排查空指针 if(head == NULL) { return -1; } struct ListNode* Slow = head; struct ListNode* Fast = head; int tmp = k; // 让块指针先走 k 步 while(tmp--) { Fast = Fast->next; } // 同时走 while(Fast) { Slow = Slow->next; Fast = Fast->next; } return Slow->val; }

3.3.3移除链表所有目标元素

方法:创建头结点+双指针

struct ListNode* removeElements(struct ListNode* head, int val) { // 排查空链表 if(head == NULL) { return NULL; } // 创建头结点 struct ListNode* pos = (struct ListNode*)malloc(sizeof(struct ListNode)); pos->next = head; struct ListNode* prev = pos; // cur 的前驱 struct ListNode* cur = head; while(cur) { if(cur->val == val) { prev->next = cur->next; free(cur); cur = prev->next; }else{ cur = cur->next; prev = prev->next; } } struct ListNode* New_head = pos->next; free(pos); // pos可以不用置空,因为出了函数生命周期就结束 return New_head; }

3.3.4反转链表

方法:不断取原链表的头结点,头插到新结点。

struct ListNode* reverseList(struct ListNode* head) { // 排查空链表 if(head == NULL) { return NULL; } // 创建新的链表 struct ListNode* new_head = (struct ListNode*)malloc(sizeof(struct ListNode)); new_head->next = NULL; struct ListNode* cur = head; while(cur) { // 拷贝结点 struct ListNode* tmp = cur; // cur 先走 cur = cur->next; // 再把先前拷贝的结点头插到新链表 tmp->next = new_head->next; new_head->next = tmp; } return new_head->next; }

3.3.5合并两个有序链表

方法:派空表+比大小插入+剩余尾插

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) { if(list1 == NULL) return list2; if(list2 == NULL) return list1; // 创建新链表 头结点 尾结点 struct ListNode* head = NULL,*tail = NULL; while(list1 && list2) { // 按从小到大顺序插入 if(list1->val < list2->val) { if(tail == NULL) { head = tail = list1; }else { tail->next = list1; tail = tail->next; } list1 = list1->next; }else{ if(tail == NULL) { head = tail = list2; }else { tail->next = list2; tail = tail->next; } list2 = list2->next; } } // 剩余的尾插 if(list1) { tail->next = list1; } if(list2) { tail->next = list2; } return head; }

最后,感谢阅读,欢迎大家交流与指正。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询