1.链表
关于顺序表的不足:
- 扩容有性能消耗且有可能存在空间浪费。
扩容时,如果扩小了,大量插入数据时,频繁扩容,性能消耗较大;如果扩大了,又会存在空间浪费。
例如当前容量为 100,满了以后增容到 200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。- 头部或中间插入数据时,需要挪动数据,降低效率
链表优点
- 按需申请内存, 不存在空间浪费
- 任意位置 O(1)时间插入删除数据
链表缺点
- 不支持下标的随机访问
总结:顺序表和链表相辅相成,使用要看具体应用场景
2.单链表 Singly Linked List
struct SListNode
{int data;struct SListNode* next;
};
void Test()
{struct SListNode* node1 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node2 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node3 = (struct SListNode*)malloc(sizeof(struct SListNode));struct SListNode* node4 = (struct SListNode*)malloc(sizeof(struct SListNode));printf("%p\n", node1);printf("%p\n", node2);printf("%p\n", node3);printf("%p\n", node4);
}
//node1 等其实只是结点地址
地址均不连续,堆上使用的空间地址由高到低
逻辑结构&物理结构
链表逻辑结构线性,但物理结构是非线性的。
pList(头结点)是指针变量,存的是第一个节点的地址
当我们看到链表的实现都有箭头指向下一个节点,但实际上是没有箭头的,只不过是把下一个节点的地址存到了当前节点的 next 的值
链表组合
- 单向双向
带头不带头
- 循环非循环
最多有 8 种组合
虽然有这么的组合,但实际中最常用的只有 2 种:
3.单链表实现
定义
typedef int SListDataType;
typedef struct SListNode // Single Link List
{SListDataType data;struct SListNode *next; //存储下一个节点的地址
} SListNode;
创建节点
SListNode *CreateNewNode(SListDataType x)
{SListNode *newNode = (SListNode *)malloc(sizeof(SListNode));if (newNode == NULL){printf("malloc newNode fail\n");exit(-1);}else{newNode->data = x;newNode->next = NULL;}return newNode;
}
二级指针
在单链表的操作函数中使用二级指针(指向指针的指针),主要是为了能够在函数内部修改调用者传入的指针变量本身的值。下面详细解释在单链表操作中使用二级指针的原因。
指针作为函数参数的基本概念:
在 C 语言里,函数参数传递是值传递。当把一个指针作为参数传递给函数时,实际上传递的是该指针的副本,也就是一个新的指针变量,它和原指针指向同一个内存地址,但它们是两个不同的变量。这意味着在函数内部修改这个指针副本的值,并不会影响到原指针的值。如果想在函数内修改这个指针,需要传递指针的地址,也就是二级指针。
打印
void SListPrint(SListNode* pList)
{//不需要assert(plist)SListNode* cur = pList;while (cur != NULL){printf("%d->", cur->data); cur = cur->next;//cur->next里面存的就是下一个结点的地址}printf("NULL\n");
}
提问:这里需要用断言吗?为什么不需要?
记住: 一定不能为空的才需要用断言 ,此处既是链表为空,那我们就什么也不打印就好了啊,断言太暴力了。
提问:这里需要传二级指针吗?为什么?
可以传,但没必要,Print 只是打印数据,不会修改数据,因此不需要传二级指针。
查找
查找不需要修改,也就不用传址调用,也就不需要传二级指针。
但如果传了也没问题。
//单链表查找
SListNode *SListFind(SListNode *plist, SListDataType x)
{SListNode *cur = plist;// while(cur != NULL)while (cur){if (cur->data == x){return cur;//查找兼具修改的作用}cur = cur->next;}return NULL;
}
尾删
没有节点,无法删除,直接 return
一个节点,直接 free 掉并置空即可,但注意 需要对实参进行操作,所以需要传实参地址,也就是传二级指针
注意:free 之后要置空,因为 free 掉的是指针指向的内容,但指针还是指向那块空间的,因此要把指针置空多个节点
多个节点时,删除尾节点需要把 prev-> next 置为 NULL 再把 tail free 掉。
如果用多个节点的代码去针对单个节点的情况,会产生解引用空指针的问题。
如果无需处理单个节点的情况,那其实也是不用传二级指针的,多个节点在已经有头结点的地址情况下是可以完成对原链表的删除操作的,只不过无法删除头结点而已。
void SListPopBack(SListNode** ppList)
{assert(ppList);//*ppList 为空时,ppList不为空,ppList是sList的地址//1.没有节点,无法删除,直接returnif(*ppList == NULL){return;//温柔检查}//2.单个节点else if((*ppList)->next == NULL){free(*ppList);*ppList = NULL;}//3.多个节点else{SListNode* prev = NULL;SListNode* tail = *ppList;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;//这里的tail是局部变量,置空了也没用,不过置了是个好习惯prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行}
}
传引用:
void SListPopBack(SListNode*& pList)
{//1.没有节点,无法删除,直接returnif(pList == NULL){return;}//2.单个节点else if((pList)->next == NULL){free(pList);pList = NULL;}//3.多个节点else{SListNode* prev = NULL;SListNode* tail = pList;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;//尾删时要将最后一个结点的上一个结点的next置为NULL才行}
}
头删
没有节点:
直接 return单个节点:
- 多个节点:
先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。
void SListPopFront(SListNode **ppList)
{assert(ppList);//ppList也有为空的可能// 1.没有节点if (*ppList == NULL){return;}// 2.单个节点// 3.多个节点//先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。//保存plist->next,如果直接free plist就找不到后面的空间了SListNode *next = (*ppList)->next;free(*ppList);//这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。*ppList = next;
}
传引用:
void SListPopFront(SListNode *&pList)
{// 1.没有节点if (pList == NULL){return;}// 2.单个节点// 3.多个节点//先写多个节点的情况,再去比较单个节点能否适用,发现恰好可以匹配。//保存plist->next,如果直接free plist就找不到后面的空间了SListNode *next = pList->next;free(pList); //这里只是释放了*ppList指向的那块空间内容,但*ppList还是指向那块空间的。pList = next;
}
头插
void SListPushFront(SListNode **pphead, SListDataType data)
{assert(pphead); // 如果链表为空,那就是*pphead为空SListNode *newNode = createNewNode(data);newNode->next = *pphead; // 新节点的next指向原头节点*pphead = newNode; // 更新头指针
}
如果头插时链表为空,也是可以的。
pList == NULL,newNode-> next 指向 NULL,然后再让 pList 指向 newNode,完美解决。
传引用的写法:
void SListPushFront(SListNode *&plist, SListDataType x)
{//即使传进来的是NULL也能解决SListNode *newNode = CreateNewNode(x);newNode->next = plist; //*pplist 其实就是 plistplist = newNode;
}
尾插
- 注意需要用二级指针,因为在判断空链表的情况时,需要对实参 sList 进行操作,才需要传址调用。
*ppList 其实就是 pList
void SListPushBack(SListNode **ppList, SListDataType x)
{//同样不需要断言空,因为本来就有可能传空链表SListNode* newNode = CreateNewNode(x);//1.空链表if(*ppList == NULL){*ppList = newNode;//也就是把newNode的地址覆盖掉sList原来的NULL地址//要修改sList必须传址调用}//2.正常链表,去找尾else{SListNode* tail = *ppList;//不能直接修改plist,plist一改就找不到链表了while (tail->next != NULL){tail = tail->next;}//出来时tail->next 指向的是NULLtail->next = newNode;}
}
带上头节点可以不用传址调用, 因为不需要修改 plist
如果不想用二级指针,也可以传引用。
void SListPushBack(SListNode *&pList, SListDataType x)
{//同样不需要断言空,因为本来就有可能传空链表SListNode* newNode = CreateNewNode(x);//1.空链表if(pList == NULL){pList = newNode;//也就是把newNode的地址覆盖掉pList原来的NULL地址//传进来空链表,要修改plist必须传址调用}//2.正常链表,去找尾else{SListNode* tail = pList;//不能直接修改plist,plist一改就找不到链表了while (tail->next != NULL){tail = tail->next;}//出来时tail->next 指向的是NULLtail->next = newNode;}
}
pos 后插入
2 种插入方式:
注意要操作顺序,如果先让 pos-> next 指向了 newNode 之后就找不到 pos 的下一个了。
第二种:
将 pos 的下一个临时保存起来就行,这样顺序就没所谓了。
//在pos后面插入
void SListInserAfter(SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);//注意顺序不要反了newNode->next = pos->next;pos->next = newNode;
}
//或者临时保存 pos->next
//在pos后面插入
void SListInserAfter(SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);SListNode* next = pos->next;//这样就无需关心顺序问题了pos->next = newNode;newNode->next = next;
}
pos 前插入
除了要让 newNode 指向 pos,还需 pos 之前的节点指向 newNode ,因此需要找到 pos 的前一个位置,只能从头开始遍历,非常麻烦且不实用。
- 多个节点的情况:
- 如果 pos 是第一个节点呢?
如果还让 prev 指向 newNode,那就发生了空指针解引用的问题。可以直接用 if 过滤掉这种情况。
pos 是第一个节点的,其实就相当于是头插,但是头插的话,需要改变实参的 sList,要让传进来的 pList 指向 newNode,因此还需要传二级指针或者传引用。
- pos 是最后一个节点呢?
和多个节点的情况相同
void SListInserBefore(SListNode** ppList, SListNode* pos, SListDataType x)
{assert(pos);assert(ppList);SListNode* newNode = CreateNewNode(x);if(*ppList == pos)//相当于头插{newNode->next = pos;*ppList = newNode;}else{SListNode* prev = NULL;SListNode* cur = *ppList;while (cur != pos){prev = cur;cur = cur->next;}prev->next = newNode;newNode->next = cur;}
}
传引用的写法:
void SListInserBefore(SListNode *&pList, SListNode *pos, SListDataType x)
{assert(pos);SListNode *newNode = CreateNewNode(x);if (pList == pos) //相当于头插{newNode->next = pos;pList = newNode;}else{SListNode *prev = NULL;SListNode *cur = pList;while (cur != pos){prev = cur;cur = cur->next;}prev->next = newNode;newNode->next = cur;}
}
提问:
在一个无头(不告诉头指针)单链表的某一个节点前面插入一个值 x, 怎么插?
这里没告诉头,就不能从头开始遍历去找 pos 的前一个了。
其实我们可以先后插,然后交换 data
pos 后擦除
只有一个节点:
没有可删除的,直接 return多个节点
先记录 pos 的下一个节点,如何让 pos 指向它的下一个节点的下一个节点,再 free 之前记录的 pos 的下一个节点并置空。
后一个为空时,同样适用。
void SListEraseAfter(SListNode *pos)
{assert(pos);//只有一个节点的情况if (pos->next == NULL){return;}else{SListNode *next = pos->next;pos->next = next->next;free(next);next = NULL;}
}
pos 擦除
多个节点,需要找 pos 位置的前一个节点 prev,然后 free pos,让 prev 指向 pos 后面的那个
pos 指向的是第一个节点,其实就相当于头删,需要改变实参,因此传二级指针或传引用。需要先保存 pList 的下一个节点,然后 free pos,再让 pList 指向 next
void SListEraseCur(SListNode** ppList, SListNode* pos)
{//pos指向第一个节点,相当于头删if(pos == *ppList){SListNode* next = (*ppList)->next;free(*ppList);*ppList = next;}else{SListNode* prev = NULL;SListNode* cur = *ppList;while (cur != pos){prev = cur;cur = cur->next;}//出来时cur指向的pos,prev指向pos前一个prev->next = cur->next;free(cur);cur = NULL;}
}
传引用:
void SListEraseCur(SListNode *&pList, SListNode *pos)
{// pos指向第一个节点,相当于头删if (pos == pList){SListNode *next = pList->next;free(pList);pList = next;}else{SListNode *prev = NULL;SListNode *cur = pList;while (cur != pos){prev = cur;cur = cur->next;}//出来时cur指向的pos,prev指向pos前一个prev->next = cur->next;free(cur);cur = NULL;}
}
注意: 在某个位置插入删除,需要配合 Find 一起使用。
销毁
void SListDestroy(SListNode** ppList)
{assert(ppList);SListNode* cur = *ppList;while (cur){SListNode* next = cur->next;free(cur);cur = next;}*ppList = NULL;//结束之后还要让pList指向空
}
传引用:
void SListDestroy(SListNode *&pList)
{assert(pList);SListNode *cur = pList;while (cur){SListNode *next = cur->next;free(cur);cur = next;}pList = NULL; //结束之后还要让pList指向空
}
4.带头双向循环链表实现
定义
// 定义链表存储的数据类型,方便后续修改
typedef int DCListDataType;// 定义链表节点结构体
typedef struct ListNode
{DCListDataType data;struct ListNode *prev;struct ListNode *next;
} ListNode;// 定义链表结构体,包含头节点指针和链表大小
typedef struct DoublyCircularList
{ListNode *head;int size;
} DoublyCircularList;
创建节点
// 创建新节点
ListNode *DCListCreateNode(DCListDataType data)
{ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));if (newNode == NULL){fprintf(stderr, "Memory allocation failed\n");exit(EXIT_FAILURE);}newNode->data = data;newNode->prev = NULL;newNode->next = NULL;return newNode;
}
初始化
// 初始化链表
DoublyCircularList *DCListInit()
{DoublyCircularList *list = (DoublyCircularList *)malloc(sizeof(DoublyCircularList));if (list == NULL){fprintf(stderr, "Memory allocation failed\n");exit(EXIT_FAILURE);}list->head = DCListCreateNode(0); // 创建头节点list->head->prev = list->head;list->head->next = list->head;list->size = 0;return list;
}
头插尾插
// 在链表头部插入节点
void DCListPushFront(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *newNode = DCListCreateNode(data);ListNode *next = list->head->next;// head -> newNode -> nextnewNode->next = next;newNode->prev = list->head;next->prev = newNode;list->head->next = newNode;list->size++;
}// 在链表尾部插入节点
void DCListPushBack(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *newNode = DCListCreateNode(data);// head tail newNodeListNode *tail = list->head->prev;newNode->prev = tail;newNode->next = list->head;tail->next = newNode;list->head->prev = newNode;list->size++;
}
头删尾删
// 删除链表头部节点
void DCListPopFront(DoublyCircularList *list)
{assert(list);assert(list->size > 0);// head toDelete secondListNode *toDelete = list->head->next;ListNode *second = toDelete->next;list->head->next = second;second->prev = list->head;free(toDelete);list->size--;
}// 删除链表尾部节点
void DCListPopBack(DoublyCircularList *list)
{assert(list);assert(list->size > 0);// head tailPrev tailListNode *tail = list->head->prev;ListNode *tailPrev = tail->prev;tailPrev->next = list->head;list->head->prev = tailPrev;free(tail);list->size--;
}
插入删除
// 在指定位置之前插入节点
void DCListInsert(ListNode *pos, DCListDataType data)
{assert(pos);// prev newNode posListNode *newNode = DCListCreateNode(data);ListNode *prev = pos->prev;newNode->next = pos;newNode->prev = prev;prev->next = newNode;pos->prev = newNode;
}// 删除指定位置的节点
void DCListErase(ListNode *pos)
{assert(pos);// prev pos nextListNode *prev = pos->prev;ListNode *next = pos->next;prev->next = next;next->prev = prev;free(pos);
}
复用
// 尾插 头插 尾删 头删 都可以复用 Insert 和 Erase
// 头插
void DCListPushFront(DoublyCircularList *list, DCListDataType data)
{// 复用DCListInsert函数assert(list);DCListInsert(list->head->next, data);
}
// 尾插
void DCListPushBack(DoublyCircularList *list, DCListDataType data)
{// 复用DCListInsert函数assert(list);DCListInsert(list->head, data);
}
// 头删
void DCListPopFront(DoublyCircularList *list)
{// 复用DCListErase函数assert(list);DCListErase(list->head->next);
}
// 尾删
void DCListPopBack(DoublyCircularList *list)
{// 复用DCListErase函数assert(list);DCListErase(list->head->prev);
}
查找
// 查找指定值的节点
ListNode *DCListFind(DoublyCircularList *list, DCListDataType data)
{assert(list);ListNode *current = list->head->next;while (current != list->head){if (current->data == data){return current;}current = current->next;}return NULL;
}
其他
// 获取链表的大小
int DCListSize(DoublyCircularList *list)
{assert(list);return list->size;
}// 判断链表是否为空
int DCListEmpty(DoublyCircularList *list)
{if (list == NULL)return 1;return list->size == 0;
}// 打印链表元素
void DCListPrint(DoublyCircularList *list)
{assert(list);printf("head<=>");ListNode *current = list->head->next;while (current != list->head){printf("%d<=>", current->data);current = current->next;}printf("\n");
}
摧毁
// 销毁链表
void DCListDestroy(DoublyCircularList *list)
{assert(list);ListNode *current = list->head->next;ListNode *next;while (current != list->head){next = current->next;free(current);current = next;}free(list->head);free(list);
}
测试
#include "doubly_circular_list.h"// 测试函数:测试链表的初始化和插入操作
void testInitializationAndInsertion()
{DoublyCircularList *list = DCListInit();// 测试在头部插入元素DCListPushFront(list, 10);DCListPushFront(list, 20);printf("在头部插入元素后的链表: ");DCListPrint(list);// 测试在尾部插入元素DCListPushBack(list, 30);DCListPushBack(list, 40);printf("在尾部插入元素后的链表: ");DCListPrint(list);// 测试链表大小printf("当前链表大小: %d\n", DCListSize(list));DCListDestroy(list);
}// 测试函数:测试链表的删除操作
void testDeletion()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 1);DCListPushBack(list, 2);DCListPushBack(list, 3);DCListPushBack(list, 4);printf("原始链表: ");DCListPrint(list);// 测试删除头部元素DCListPopFront(list);printf("删除头部元素后的链表: ");DCListPrint(list);// 测试删除尾部元素DCListPopBack(list);printf("删除尾部元素后的链表: ");DCListPrint(list);// 测试删除指定位置元素ListNode *pos = DCListFind(list, 2);if (pos != NULL){DCListErase(pos);}printf("删除元素 2 后的链表: ");DCListPrint(list);DCListDestroy(list);
}// 测试函数:测试链表的查找操作
void testFinding()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 100);DCListPushBack(list, 200);DCListPushBack(list, 300);printf("当前链表: ");DCListPrint(list);ListNode *found = DCListFind(list, 200);if (found != NULL){printf("找到元素 200\n");}else{printf("未找到元素 200\n");}found = DCListFind(list, 400);if (found != NULL){printf("找到元素 400\n");}else{printf("未找到元素 400\n");}DCListDestroy(list);
}// 测试函数:测试在指定位置插入元素
void testInsertAtPosition()
{DoublyCircularList *list = DCListInit();DCListPushBack(list, 1);DCListPushBack(list, 3);printf("原始链表: ");DCListPrint(list);ListNode *pos = DCListFind(list, 3);if (pos != NULL){DCListInsert(pos, 2);}printf("在元素 3 前插入元素 2 后的链表: ");DCListPrint(list);DCListDestroy(list);
}// 测试函数:测试链表是否为空
void testEmpty()
{DoublyCircularList *list = DCListInit();if (DCListEmpty(list)){printf("链表初始为空\n");}else{printf("链表初始不为空,测试错误\n");}DCListPushBack(list, 1);if (DCListEmpty(list)){printf("插入元素后链表仍为空,测试错误\n");}else{printf("插入元素后链表不为空\n");}DCListDestroy(list);
}int main()
{testInitializationAndInsertion();printf("\n");testDeletion();printf("\n");testFinding();printf("\n");testInsertAtPosition();printf("\n");testEmpty();return 0;
}
测试结果:
(base) PS C:\Users\yzq0207\OneDrive\learn\programming\dataStructure> & 'c:\Users\yzq0207\.vscode\extensions\ms-vscode.cpptools-1.24.2-win32-x64\debugAdapters\bin\WindowsDebugLauncher.exe' '--stdin=Microsoft-MIEngine-In-i5hnyga5.grr' '--stdout=Microsoft-MIEngine-Out-oxcfklgl.m5e' '--stderr=Microsoft-MIEngine-Error-z01squvk.zmt' '--pid=Microsoft-MIEngine-Pid-prir2imd.yj0' '--dbgExe=C:\Program Files\mingw64\bin\gdb.exe' '--interpreter=mi'
在头部插入元素后的链表: head<=>20<=>10<=>
在尾部插入元素后的链表: head<=>20<=>10<=>30<=>40<=>
当前链表大小: 4原始链表: head<=>1<=>2<=>3<=>4<=>
删除头部元素后的链表: head<=>2<=>3<=>4<=>
删除尾部元素后的链表: head<=>2<=>3<=>
删除元素 2 后的链表: head<=>3<=>当前链表: head<=>100<=>200<=>300<=>
找到元素 200
未找到元素 400原始链表: head<=>1<=>3<=>
在元素 3 前插入元素 2 后的链表: head<=>1<=>2<=>3<=>链表初始为空
插入元素后链表不为空
5.源代码
顺序表