1. 链表的概念及优缺点
1.1 链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,
数组元素的逻辑顺序是通过链表中的指针链接次序实现的。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向、双向
2. 带头、不带头
3. 循环、非循环
1. 无头单向非循环链表:(下面实现的就是这种结构)
结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,
如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,
都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
1.2 顺序表和链表的优缺点
为什么顺序表和链表各自存在?
因为他们各有优点,他们是互补的,并不是有他没我,有我没他的。
顺序表的优点:
① 支持随机访问,有些算法需要结构随机访问,比如二分查找和优化的快排等。
② 数据是按顺序存放的,空间利用率高。
③ 通过下标直接访问,存取速度高效。
顺序表的缺点:
① 中间/头部的插入删除,需要挪动,挪动数据时也是存在消耗的,时间复杂度为O(N)
② 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
③ 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,
我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
链表的优点:① 按需申请空间,不用就释放空间( 链表可以更合理地使用空间)。
② 头部或者中间位置的插入和删除,不需要挪动数据。
③ 不存在空间浪费。
链表的缺陷:
① 每一个数据,都要存一个指针去链接后面的数据节点。
② 不支持随机访问(用下标直接访问第 i 个),必须得走 O(N) 。
单链表的缺陷还是很多的,单纯单链表的增删查找的意义不大,
但是很多OJ题考察的都是单链表。单链表多用于更复杂的数据结构的子结构,
如哈希桶,邻接表等。所以真正存储数据还是得靠双向链表。
2. 链表的实现(完整代码)
还是和上一篇顺序表的实现一样,主要实现增删查改功能,可以一个一个函数看,很简单。
Slist.h
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLNDataType; typedef struct SlistNode //single单一的 node节点 { SLNDataType data; struct SlistNode* next; }SLNode; void SListPrint(SLNode* phead); void SListPushBack(SLNode** pphead, SLNDataType x); void SListPushFront(SLNode** pphead, SLNDataType x); SLNode* CreateNewNode(SLNDataType x); void SListPopBack(SLNode** pphead); void SListPopFront(SLNode** pphead); SLNode* SListFind(SLNode* phead, SLNDataType x); void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x); void SListInsertAfter(SLNode* pos, SLNDataType x); void SListEarse(SLNode** pphead, SLNode* pos); void SListEarseAfter(SLNode* pos); void SListDestroy(SLNode** pphead);
SList.c
#include"SList.h" void SListPrint(SLNode* phead) { SLNode* cur = phead; while (cur != NULL) { printf("%d->", cur->data); cur = cur->next; } printf("NULL\n"); } void SListPushBack(SLNode** pphead, SLNDataType x) { SLNode* NewNode = CreateNewNode(x); if (*pphead == NULL) { *pphead = NewNode; } else { SLNode* tail = *pphead;//tail 尾巴 while (tail->next != NULL)//找到尾节点 { tail = tail->next; } tail->next = NewNode; } } SLNode* CreateNewNode(SLNDataType x) { SLNode* NewNode = (SLNode*)malloc(sizeof(SLNode)); if (NewNode == NULL) { printf("malloc failed!\n"); exit(-1); } NewNode->data = x; NewNode->next = NULL; return NewNode; } void SListPushFront(SLNode** pphead, SLNDataType x) { SLNode* NewNode = CreateNewNode(x); NewNode->next = *pphead; *pphead = NewNode; } void SListPopBack(SLNode** pphead) { assert(*pphead != NULL); if ((*pphead)->next == NULL)//如果只有一个节点 { free(*pphead); *pphead = NULL; } else { SLNode* tail = *pphead; while (tail->next->next) { tail = tail->next; } free(tail->next); tail->next = NULL; } } void SListPopFront(SLNode** pphead) { assert(*pphead != NULL); SLNode* begin = *pphead; *pphead = (*pphead)->next; free(begin); begin = NULL; } SLNode* SListFind(SLNode* phead, SLNDataType x) { SLNode* cur = phead; while (cur != NULL) { if (cur->data == x) { return cur; } else { cur = cur->next; } } return NULL; } void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x) { SLNode* NewNode = CreateNewNode(x); if (*pphead == pos) { NewNode->next = *pphead; *pphead = NewNode; } else { SLNode* posPrev = *pphead; while (posPrev->next != pos) { posPrev = posPrev->next; } posPrev->next = NewNode; NewNode->next = pos; } } void SListInsertAfter(SLNode* pos, SLNDataType x) { assert(pos != NULL); SLNode* NewNode = CreateNewNode(x); NewNode->next = pos->next; pos->next = NewNode; //和前面对比,单链表的缺陷是要找pos的前一个要从头往后找 } void SListEarse(SLNode** pphead, SLNode* pos) { assert(*pphead != NULL); assert(pos != NULL); if (*pphead == pos)//如果是头删 { *pphead = pos->next; free(pos); pos = NULL; } else { SLNode* posPrev = *pphead; while (posPrev->next != pos) { posPrev = posPrev->next; } posPrev->next = pos->next; free(pos); pos = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁,但是个好习惯emm } } void SListEarseAfter(SLNode* pos) { //1 2 3 4 assert(pos != NULL); assert(pos->next != NULL); SLNode* posAfter= pos->next; pos->next = posAfter->next; free(posAfter); posAfter = NULL;//可以不写,形参不会改变实参,这个野指针会被栈帧销毁 } void SListDestroy(SLNode** pphead) { assert(pphead); SLNode* cur = *pphead; while (cur) { SLNode* next = cur->next; free(cur); cur = next; } *pphead = NULL; }
Test.c
#define _CRT_SECURE_NO_WARNINGS 1 #include"SList.h" void TestSList1()//测试尾插头插 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); SListPushFront(&pList, 1); SListPushFront(&pList, 2); SListPushFront(&pList, 3); SListPushFront(&pList, 4); SListPrint(pList); } void TestSList2()//测试尾删头删 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); SListPopBack(&pList); SListPrint(pList); SListPopFront(&pList); SListPrint(pList); SListPopBack(&pList); SListPopBack(&pList); SListPrint(pList); } void TestSList3()//测试查找函数 { SLNode* pList = NULL; SListPushFront(&pList, 1); SListPushFront(&pList, 2); SListPushFront(&pList, 3); SListPushFront(&pList, 2); SListPushFront(&pList, 4); SListPushFront(&pList, 2); SListPushFront(&pList, 2); SListPushFront(&pList, 4); SListPrint(pList); SLNode* pos = SListFind(pList, 2); for (int i = 0;pos != NULL;i++) { printf("第%d个pos节点:%p->%d\n", i, pos, pos->data); pos->data = 9;//查找过程中把2改成9 返回SLNode*的好处 pos = SListFind(pos->next, 2); } SListPrint(pList); } void TestSList4()//测试指定位置的前面插入 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); SLNode* pos = SListFind(pList, 2); SListInsert(&pList, pos, 60); SListPrint(pList); pos = SListFind(pList, 1); SListInsert(&pList, pos, 70); SListPrint(pList); SListInsert(&pList, NULL, 80); SListPrint(pList); } void TestSList5()//测试指定位置的后面插入 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); SLNode* pos = SListFind(pList, 3); SListInsertAfter(pos, 10); SListPrint(pList); pos = SListFind(pList, 4); SListInsertAfter(pos, 20); SListPrint(pList); pos = SListFind(pList, 1); SListInsertAfter(pos, 30); SListPrint(pList); } void TestSList6()//测试指定位置的删除 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); SLNode* pos = SListFind(pList, 3); SListEarse(&pList, pos); SListPrint(pList); pos = SListFind(pList, 1); SListEarse(&pList, pos); SListPrint(pList); pos = SListFind(pList, 2); SListEarse(&pList, pos); SListPrint(pList); pos = SListFind(pList, 4); SListEarse(&pList, pos); SListPrint(pList); } void TestSList7()//测试指定位置之后的删除和整个链表销毁 { SLNode* pList = NULL; SListPushBack(&pList, 1); SListPushBack(&pList, 2); SListPushBack(&pList, 3); SListPushBack(&pList, 4); SListPrint(pList); //SListDestroy(&pList); //SListPrint(pList); SLNode* pos = SListFind(pList, 3); SListEarseAfter(pos); SListPrint(pList); pos = SListFind(pList, 1); SListEarseAfter(pos); SListPrint(pList); pos = SListFind(pList, 1); SListEarseAfter(pos); SListPrint(pList); SListDestroy(&pList); SListPrint(pList); } int main() { //TestSList1();//测试尾插头插 //TestSList2();//测试尾删头删 //TestSList3();//测试查找函数 //TestSList4();//测试指定位置的前面插入 //TestSList5();//测试指定位置的后面插入 //TestSList6();//测试指定位置的删除 TestSList7();//测试指定位置之后的删除和整个链表销毁 return 0; }
3. 笔试选择题练习
练习1
下列关于链表的说法哪个是正确的?( )
A.插入或删除时,无需移动其他元素
B.数据在内存中一定是连续的
C.需要事先估计存储空间
D.可以随机访问表内的元素
练习2
在单链表指针为p的结点之后插入指针为s的结点,正确的操作是( )
A.p->next=s; s->next=p->next;
B.p->next=s->next; p->next=s;
C.s->next=p->next; p->next=s;
D.p->next=s->next; p->next=s;
练习3
在一个单链表中,q 的前一个节点为 p,删除 q 所指向节点时,
以下代码正确的执行语句及次序是( )
①`q->next=p->next;`
②`p->next=q->next;`
③`delete p;`
④`delete q;`
A.②③
B.④
C.①④
D.②④
练习4
在长度为n(n>1)的单链表上,设有头和尾两个指针,执行( )操作与链表的长度有关。
A.在单链表第一个元素前插入一个新元素
B.在单链表最后一个元素后插入一个新元素
C.删除单链表中的第一个元素
D.删除单链表中的最后一个元素
练习5
自己完成下面的接口函数
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLNDataType; typedef struct SlistNode //single单一的 node节点 { SLNDataType data; struct SlistNode* next; }SLNode; void SListPrint(SLNode* phead); void SListPushBack(SLNode** pphead, SLNDataType x); void SListPushFront(SLNode** pphead, SLNDataType x); SLNode* CreateNewNode(SLNDataType x); void SListPopBack(SLNode** pphead); void SListPopFront(SLNode** pphead); SLNode* SListFind(SLNode* phead, SLNDataType x); void SListInsert(SLNode** pphead, SLNode* pos, SLNDataType x); void SListInsertAfter(SLNode* pos, SLNDataType x); void SListEarse(SLNode** pphead, SLNode* pos); void SListEarseAfter(SLNode* pos); void SListDestroy(SLNode** pphead);
答案及解析
练习1答案:A
解析:链表插入删除元素只需要修改指针指向,不需要移动元素。链表是用指针链接前后节点,数据在内存中不一定连续,每次插入元素,都是先出开一个节点的空间,不需要事先估计存储空间,并且链表没有索引,不能随机访问。
练习2答案:C
解析:先把s和p的next节点链接,再更新p的next为s
练习3答案:D
解析: 首先要更新链接,再删除q节点
练习4答案:D
解析:A选项为头插,不需要遍历链表,B选项为尾插,也不需要遍历链表,C选项为头删,不需要遍历链表,只有D选项,为尾删,需要遍历单链表,找到尾节点的前一个节点。所以与长度有关。
练习5答案参照上面SList.c