数据结构之链表
Posted 玄鸟轩墨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之链表相关的知识,希望对你有一定的参考价值。
写在前面
谈完了顺序表,这就到了链表了。在这里我们不得不提一下链表的优点。没办法,优秀的事物总是比较出来的。今天的任务不简单。我们要学习知识很多。本且这也是我们在找工作的时候必考常考的知识点。
链表
在《漫画算法:小灰的算法之旅》一书中,举了一个很有趣的例子,它把顺序表比作正规军,链表就是地下党,这是很形象的,顺序表的物理结构是一个数组,我们是顺序存储,但是链表是随机存储的,我们有的时候只能找到它的上级和下级,不能跳跃寻找,这就像地下工作人员一样。
由于数据结构这个模块大部分使用C语言来写的,要求我们学习这个我们需要对结构体、指针以及内存管理有充分的了解。所以说我们的C语言知识基础一定要扎实.
什么是链表
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中指针链接次序实现的 。
下面的是一个结构体
typedef int SLDataType; // 数据类型
typedef struct SListNode
SLDataType val;
struct SListNode* next; //记录下一个节点的地址
SListNode;
我们来看看一个完整的单向链表,下面就是我们要学习的知识点
为何存在链表
我们很疑惑,既然存在来了顺序表。我们为什么还要链表呢。这是由于顺序表存在一定的缺陷
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
我们看看增容的缺陷 当我们使用realloc来进行增容时,要是后面增加的空间太大,会出现==异地扩容==,这就出现了不必要的消耗
void test()
int* p1 = (int*)malloc(40);
printf("%p\\n", p1);
p1 = (int*)realloc(p1, 100000);
printf("%p\\n", p1);
free(p1);
int main()
test();
return 0;
链表分类
链表也是有一定分类的,不过本质都一样,组合起来分为八种
- 是否带头
- 单向还是双向
- 是否循环
单链表
我们先说一下单向不带头的链表,先学会基础的后面大家看起来会比较容易。
打印
为了测试代码的正确性,我们先看看如何打印单链表,后面要辅助我们验证代码的正确性
void SListPrint(SListNode* pHead)
SListNode* cur = pHead;
while (cur != NULL)
printf("%d ->", cur->val);
cur = cur->next;
printf("NULL\\n");
插入数据
一般而言,单链表数据的插入分为头插和尾插两种,我们一般很少使用其他的插入方式,不是说这些方式不能实现,而是没有太大的价值.
头插
- 链表是一个空链表,插入的节点就是头节点
- 链表是一个正常的,插入一个节点.
- 为何传入的是二级指针? 要是我们第一次头插,需要改变头节点的地址,传入一级指针(头节点的地址),改变的是形参
- 为何定义BuySListNode(x);,我们发现,后面的尾插也需要创造一个节点,避免代码的重复造轮子,我们单独拿出来
SListNode* BuySListNode(SLDataType x)
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
if (node == NULL)
printf("malloc fail!\\n");
exit(-1);
node->next = NULL;
node->val = x;
return node;
void SListPushFront(SListNode** pplist, SLDataType x)
assert(pplist);
SListNode* node = BuySListNode(x);
//判断是不是头一次插入
if (*pplist == NULL)
*pplist = node;
return;
node->next = *pplist;
*pplist = node;
int main()
SListNode* pHead = NULL;
SListPrint(pHead);
SListPushFront(&pHead, 3);
SListPushFront(&pHead, 2);
SListPushFront(&pHead, 1);
SListPrint(pHead);
return 0;
尾插
头插说完了,我们要说说尾插,一般而言,尾插要比头插应用的更多一点,我们一般都是在尾部插入数据,这样更符合我们的想法.
void SListPushBack(SListNode** ppHead, SLDataType x)
assert(ppHead);
SListNode* node = BuySListNode(x);
// 第一次插入
if (*ppHead == NULL)
*ppHead = node;
return;
SListNode* cur = *ppHead;
while (cur->next != NULL)
cur = cur->next;
cur->next = node;
删除数据
插入数据已经说完了,现在我们看看如何删除数据.
头删
头删就是删除第一个节点,在顺序表中,我们可能要移动很多的数据,但是单链表不需要,你会发现它的优点
void SListPopFront(SListNode** ppHead)
assert(ppHead);
if (*ppHead == NULL)
printf("没有节点");
return;
SListNode* cur = (*ppHead)->next;
free(*ppHead);
*ppHead = cur;
int main()
SListNode* pHead = NULL;
SListPushBack(&pHead, 1);
SListPushBack(&pHead, 2);
SListPushBack(&pHead, 3);
SListPrint(pHead);
SListPopFront(&pHead);
SListPrint(pHead);
SListDestory(&pHead);
return 0;
尾删
链表的尾删和尾插一样,都需要找到最后一个节点,不过尾删需要把倒数第二个节点的next置为NULL,这是一定需要的做的.
void SListPopBack(SListNode** ppHead)
assert(ppHead);
if (*ppHead == NULL)
printf("没有节点");
return;
SListNode* cur = *ppHead;
// 只有 一个节点
if (cur->next == NULL)
free(cur);
*ppHead = NULL;
return;
SListNode* curNext = cur->next;
while (curNext->next != NULL)
cur = curNext;
curNext = curNext->next;
free(curNext);
cur->next = NULL;
int main()
SListNode* pHead = NULL;
SListPushBack(&pHead, 1);
SListPushBack(&pHead, 2);
SListPushBack(&pHead, 3);
SListPrint(pHead);
SListPopBack(&pHead);
SListPrint(pHead);
SListDestory(&pHead);
return 0;
查找数据
我们可以在单链表中查看val的值是不是一样的,一样的话就返回这个节点的地址,不是就返回NULL,数据的查找在现实生活中很大的作用
SListNode* SListFind(SListNode* pHead, SLDataType key)
assert(pHead);
SListNode* cur = pHead;
while (cur != NULL)
if (cur->val == key)
return cur;
cur = cur->next;
return NULL;
int main()
SListNode* pHead = NULL;
SListPushBack(&pHead, 1);
SListPushBack(&pHead, 2);
SListPushBack(&pHead, 3);
SListNode* ret = SListFind(pHead, 2);
if (ret != NULL)
printf("%d", ret->val);
SListDestory(&pHead);
return 0;
单链表在pos位置之后插入x
前面我们就谈了查找数据时就返回了节点的地址,做的铺垫就是为了这个.不过我们要考虑一下,为何不在pos前面插入数据呢?很简单,我们使用的是单链表,得到后面的节点的地址很容易,但是想要得到前面的地址,你就慢慢玩吧.
void SListInsertAfter(SListNode* pos, SLDataType x)
assert(pos);
SListNode* cur = pos->next;
SListNode* node = BuySListNode(x);
pos->next = node;
node->next = cur;
int main()
SListNode* pHead = NULL;
SListPushBack(&pHead, 1);
SListPushBack(&pHead, 2);
SListPushBack(&pHead, 3);
SListPrint(pHead);
SListNode* ret = SListFind(pHead, 2);
SListInsertAfter(ret,0);
SListPrint(pHead);
SListDestory(&pHead);
return 0;
单链表删除pos位置之后的值
我们刚才说了pos后面插入,现在应该说说删除,这些都是很简单的
void SListEraseAfter(SListNode* pos)
assert(pos);
SListNode* cur = pos->next;
//判断 pos 是不是 尾节点
if (cur == NULL)
return;
pos->next = cur->next;
free(cur);
int main()
SListNode* pHead = NULL;
SListPushBack(&pHead, 1);
SListPushBack(&pHead, 2);
SListPushBack(&pHead, 3);
SListPrint(pHead);
SListNode* ret = SListFind(pHead, 1);
SListEraseAfter(ret);
SListPrint(pHead);
SListDestory(&pHead);
return 0;
销毁链表
有的人可能会疑惑,我们为何要最后销毁链表呢?我们节点的空间都是malloc出来的,使用完比后我们要释放,不然会出现内存泄漏.或许你觉得这点内存的没啥,但是要是我们链表过长,次数过多,服务器的内存会一点一点变小.最后你就被领导骂了.
void SListDestory(SListNode** ppHead)
assert(ppHead);
SListNode* cur = *ppHead;
while (cur != NULL)
SListNode* curNext = cur->next;
free(cur);
cur = curNext;
*ppHead = NULL;
双向带头循环链表
单链表我们就已经说的很详细了,不过单链表还是有一定的缺陷,
- 不能得到前面节点的地址
- 尾插的时候我们需要遍历整个链表,时间复杂度O(N).
这些都是问题,我,我们想一想是不是有个链表来解决这些问题.很高兴双向循环链表可以解决这些问题.我们这里的方法和上面的几乎是一样的,这里就不加动图了.不过我会用文字详细的描述出来.
节点
typedef int LTDataType;
typedef struct ListNode
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
ListNode;
创造头节点
上面的单链表我们没有添加标兵位,这里添加一下吧.
ListNode* ListCreate()
ListNode* cur = (ListNode*)malloc(sizeof(ListNode));
assert(cur);
cur->data = -1;
cur->next = cur;
cur->prev = cur;
return cur;
int main()
ListNode* paceSetter = ListCreate(); //标兵
return 0;
打印
和前面一样,我们需要测试代码的正确性,这里先给出打印的函数。
- 创造一个节点,指向标兵位的next
- 打印这个数值,这个节点一直往后移,直到节点的地址等于标兵位
void ListPrint(ListNode* plist)
assert(plist);
ListNode* cur = plist->next;
while (cur != plist)
printf("%d->", cur->data);
cur = cur->next;
printf("NULL\\n");
插入元素
头插
需要完成一下步骤,
- 创造一个节点
- 新节点的next保存原来的头节点
- 原来的头节点的prev保存新节点
- 标兵位next保存新节点的位置,新节点的prev保存标兵位
void ListPushFront(ListNode* plist, LTDataType x)
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
assert(node);
node->data = x;
node->next = plist->next;
node->next->prev = node;
plist->next = node;
node->prev = plist;
尾插
尾插更简单,使用单链表的时候还需要一步一步寻找尾节点,时间复杂度O(N)这里不需要,我们可以观察到标兵位的prev就是尾节点的地址。
- 创造一个节点
- 拿一个节点指向尾节点
- 依次修改尾节点和标兵位相应的值
void ListPushBack(ListNode* plist, LTDataType x)
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
assert(node);
node->data = x;
ListNode* tail = plist->prev;
//尾节点的next指向 node
tail->next = node;
//node的prev指向 tail
node->prev = tail;
//node的next指向标兵位
node->next = plist;
//标兵位的prev指向node
plist->prev = node;
删除元素
头删
记住头删不是删除标兵位,是有效节点。
- 拿个节点记录头节点,判断头节点是不是和标兵位相同
- 记录头节点的下一个节点,依次修改相应的值。
void ListPopFront(ListNode* plist)
assert(plist);
ListNode* head = plist->next;
if (head == plist)
return;
ListNode* headNext = head->next;
head->next = NULL;
head->prev = NULL;
free(head);
plist->next = headNext;
headNext->prev = plist;
尾删
尾删很简单,我们找到最后的尾节点,一步修改相应的next和prev就可以了在,这里就不和大家分享了。
void ListPopBack(ListNode* plist)
assert(plist);
ListNode* tail = plist->prev;
if (tail == plist)
return;
tail->prev->next = plist;
plist->prev = tail->prev;
tail->next = NULL;
tail->prev = NULL;
free(tail);
链表总结
到这里,大家应该对链表的理论有一定的了解了吧,不过这是远远不够的,我们需要一定的习题来练习,锻炼我们的思维。这里我把一些简单的OJ题给大家列出来吧,里面只有答案,没有讲解,不过这些都是比较简单的,大家坐一坐,提高一下能力。
以上是关于数据结构之链表的主要内容,如果未能解决你的问题,请参考以下文章