【数据结构】链表解析与实战运用(1.8w字超详细解析)

目录

引言

链表概念及结构

链表的优缺点

链表的分类

1.单向或者双向

2.带头或者不带头

3.带循环或者非循环

单链表接口函数的实现

接口函数一览

创建空节点&打印链表

尾部插入

头部插入

尾部删除

头部删除

查找

在pos位置之后插入节点

在pos位置之前插入节点

删除pos位置的节点

删除pos位置的后一个节点

销毁链表

单链表调试必备技巧

单链表实战运用

移除链表元素

反转链表

链表的中间节点

链表中倒数第k个节点

合并两个有序链表

链表分割

相交链表

环形链表1:是否带环

带环延伸问题

环形链表2:环的入口点

复杂链表的复制

双向链表

双向链表接口函数的实现

接口函数一览

创建哨兵位头节点与打印链表

创建一个新节点

数据节点插入

数据节点删除

尾插与尾删

头插与头删

数据查找(返回pos指针)

销毁链表

总结

链表与顺序表的优劣

额外拓展


引言

        承接上文(顺序表),为了弥补顺序表的缺陷,设计出了链表,按需申请空间,不用了就释放空间(更合理地使用了空间)。(以单链表为例)存储一个数据,然后在这个数据后紧跟一个指向下一个数据的指针就可以将这一串数据节点连接起来,而结束时就在末尾位置放一个空指针就好了,这样很大程度上改善了顺序表头部和中间插入或删除的数据挪动问题,当要进行数据插入时,直接更改前后连接的指针就可以了。(下图所示单链表)

链表概念及结构

概念:

        链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

注意:

        1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续

        2.现实中的结点一般都是从堆上申请出来的

        3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

假设在32位系统上,节点中值为int类型,则一个节点的大小为8字节,则也可能有下图链表:

链表的优缺点

优点:

        1.按需申请空间,不用了就释放(更合理地使用了空间)

        2.头部中间插入删除数据,不需要挪动数据

        3.不存在空间浪费

缺点:

        1.每一个数据,都要存指针去链接后面或者前面的数据节点

        2.不支持随机访问(用下标直接访问第 i 个)

链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8中链表情况:

1.单向或者双向

2.带头或者不带头

3.带循环或者非循环

虽然有这么多的链表的结构,但是我们实际中最常用的还是两种结构:

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

(下文中以这两种经典结构为例子进行分析)

单链表接口函数的实现

接口函数一览

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "SList.h"typedef int SLTDatatype;//无头单向非循环
typedef struct SlistNode
{int data;struct SlistNode* next;
}SLTNode;SLTNode* CreateListNode(SLTDatatype x); //创建一个节点,next为空
void SListPrint(SLTNode* plist);//打印链表
void SListPushBack(SLTNode** pplist, SLTDatatype x);//尾部插入
void SListPushFront(SLTNode** pplist, SLTDatatype x);//头部插入
void SListPopBack(SLTNode** pplist);//尾部删除
void SListPopFront(SLTNode** pplist);//头部删除
//查找,返回一个位置pos
SLTNode* SListFind(SLTNode* plist, SLTDatatype x);
//在pos位置之前去插入一个节点
void SListInsert(SLTNode** plist, SLTNode* pos, SLTDatatype x);
//在pos位置之后去插入一个节点(更适合单链表,也更简单)
void SListInsertAfter(SLTNode* pos, SLTDatatype x);
//将pos位置的节点删除
void SListErase(SLTNode** plist, SLTNode* pos);
//将pos位置后一个的节点删除(更简单,但是有缺陷)
void SListEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** plist);

创建空节点&打印链表

//创建一个节点,next为空
SLTNode* CreateListNode(SLTDatatype x)
{//新开一个节点,并将next置为空指针SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL)//防止内存申请失败{printf("malloc failed!\n");exit(-1);}newnode->data = x;newnode->next = NULL;//返回节点地址return newnode;
}//打印链表
void SListPrint(SLTNode* plist)
{SLTNode* cur = plist;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

尾部插入

//尾部插入
void SListPushBack(SLTNode** pplist, SLTDatatype x)
{assert(pplist);//正确传参绝对不为空,此处断言是为了防止传参错误:&plist传成plist//创建一个新节点SLTNode* newnode = CreateListNode(x);//检测是否是一个从0开始的新链表if (*pplist == NULL){*pplist = newnode;}else{//找到尾节点SLTNode* tail = *pplist;while (tail->next != NULL){tail = tail->next;}//使原尾部节点next存新尾部节点的地址tail->next = newnode;}
}

头部插入

//头部插入
void SListPushFront(SLTNode** pplist, SLTDatatype x)
{assert(pplist);//正确传参绝对不为空,此处断言是为了防止传参错误:&plist传成plist//创建一个新节点SLTNode* newnode = CreateListNode(x);//使新节点指向原第一个节点,使plist指向新开的节点newnode->next = *pplist;*pplist = newnode;
}

尾部删除

//尾部删除
void SListPopBack(SLTNode** pplist)
{assert(*pplist);//链表为空不能删除SLTNode* newend = NULL;SLTNode* tail = *pplist;while (tail->next != NULL)//节点next不为空,储存该节点地址,进入下一个节点{newend = tail;tail = tail->next;}free(tail);//释放最后一个节点空间tail = NULL;if (newend != NULL)//节点大于一个,进入while循环newend被赋值newend->next = NULL;else               //节点只有一个,没进入while循环newend为空*pplist = NULL;
}

头部删除

//头部删除
void SListPopFront(SLTNode** pplist)
{assert(*pplist);//防止链表为空SLTNode* tail = (*pplist)->next;//存储第二个节点地址free(*pplist);                    //释放第一个节点空间*pplist = tail;
}

查找

//查找,返回一个位置pos
//具体用法详见下方Test3函数
SLTNode* SListFind(SLTNode* plist, SLTDatatype x)
{assert(plist);SLTNode* cur = plist;while (cur){if (cur->data == x){return cur;}else{cur = cur->next;}}return NULL;
}void TestSList3()//查找函数的诸多用法
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 2);SListPushFront(&plist, 5);SListPrint(plist);int i = 1;SLTNode* pos = SListFind(plist, 2);while (pos)//链表中有多个要查找的值{printf("第%d个pos节点:%p->%d\n",i++,pos,pos->data);pos = SListFind(pos->next, 2);}//修改pos = SListFind(plist, 2);if (pos){pos->data = 30;}SListPrint(plist);
}

在pos位置之后插入节点

//在pos位置之后去插入一个节点(更适合单链表,也更简单)
void SListInsertAfter(SLTNode* pos, SLTDatatype x)
{assert(pos);SLTNode* newnode = CreateListNode(x);newnode->next = pos->next;pos->next = newnode;
}

在pos位置之前插入节点

//在pos位置之前去插入一个节点
//具体用法详见Test4
void SListInsert(SLTNode** pplist, SLTNode* pos, SLTDatatype x)
{assert(pplist);//正确传参绝对不为空,此处断言是为了防止传参错误:&plist传成plistassert(pos);SLTNode* newnode = CreateListNode(x);if (*pplist == pos)//头插{newnode->next = *pplist;*pplist = newnode;}else{//找到pos的前一个位置SLTNode* posPrev = *pplist;while (posPrev->next != pos){posPrev = posPrev->next;}posPrev->next = newnode;newnode->next = pos;}
}void TestSList4()
{SLTNode* plist = NULL;SListPushFront(&plist, 1);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 4);SListPushFront(&plist, 2);SListPushFront(&plist, 3);SListPushFront(&plist, 2);SListPushFront(&plist, 5);SListPrint(plist);SLTNode* pos = SListFind(plist, 3);if (pos){SListInsert(&plist, pos, 30);}SListPrint(plist);
}

删除pos位置的节点

//将pos位置的节点删除
void SListErase(SLTNode** pplist, SLTNode* pos)
{assert(*pplist);assert(pos);if (pos == *pplist)//头删{*pplist = pos->next;free(pos);pos = NULL;}else//中间和尾部的节点删除{SLTNode* cur = *pplist;while (cur->next != pos){cur = cur->next;}cur = pos->next;free(pos);pos = NULL;}
}

删除pos位置的后一个节点

//删除pos位置的后一个节点
void SListEraseAfter(SLTNode* pos)
{assert(pos->next);SLTNode* next = pos->next->next;free(pos->next);pos->next = next;
}

销毁链表

//销毁链表
void SListDestroy(SLTNode** pplist)
{assert(pplist);if (*pplist == NULL){return;}else{SLTNode* cur = *pplist;SLTNode* next = cur->next;while (cur){next = cur->next;free(cur);cur = next;}*pplist = NULL;}
}

单链表调试必备技巧

手动创建一个链表,而不用写头插尾插函数:

struct ListNode
{int val;struct ListNode* next;
}//方便快速调试oj代码
struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
n1->val = 7;
n2->val = 7;
n3->val = 7;
n4->val = 7;
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = NULL;

单链表实战运用

        了解完单链表的基本增删查改之后就进入到单链表的实战运用,一般不会直接拿单链表来存储数据,一般是用作其他数据结构的子结构,或者是作为一道oj题出现。在运用和练习中才可以更好地将知识学会。

移除链表元素

这道题很简单,用两个指针分别记录一前一后两个位置,让前一个指针的next指向该指针往下第二个位置,然后将中间那个指针指向的空间释放掉,就可以达到删除的效果了。

代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* prev = NULL;struct ListNode* cur = head;while (cur){if (cur->val == val){if (cur == head)//头删{cur = head->next;free(head);head = cur;}else{//中间删prev->next = cur->next;free(cur);cur = prev->next;}}else{//迭代往后走prev = cur;cur = cur->next;}}return head;
}

反转链表

思路一:在原先基础上一个一个翻转

使用n1,n2来改变相邻两个节点的顺序,使用n3来记录下一个节点的位置,方便迭代。

迭代:n2->next = n1,然后n1 = n2,n2 = n3,n3 = n2->next。

当n2为空时说明刚刚已经完成了最后一对节点的反转,反转完成后即可跳出结束循环,此时n1为链表新的头。

实现代码:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* reverseList(struct ListNode* head)
{if (head == NULL){return NULL;}else{struct ListNode* n1, * n2, * n3;n1 = NULL;n2 = head;n3 = head->next;while (n2){//翻转n2->next = n1;//迭代往后走n1 = n2;n2 = n3;if(n3)n3 = n3->next;}return n1;}
}

思路二:头插法

思路:

        取原链表中节点,头插到newHead新链表中,用一个cur指针头插新节点,用一个next指针记录下一个节点。

将cur节点头插进新链表中成为新的头,然后cur = next,next = next->next,再将cur头插,重复上一次的行为,往下迭代。

当cur为空时,newHead新链表如下图所示。即cur为空是循环的结束条件。

代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* reverseList(struct ListNode* head)
{struct ListNode* cur = head;struct ListNode* next = NULL;struct ListNode* newHead = NULL;while (cur){next = cur->next;//头插cur->next = newHead;newHead = cur;//迭代往后走cur = next;}return newHead;
}

链表的中间节点

思路非常简单:

        将链表遍历一遍,计数,得到链表长度后除以2,然后再遍历 链表长度/2 个节点就可以得到中间节点了。

但是如果现在要求只能遍历链表1次,那该怎么解答呢?

有一种经典的解法叫做快慢指针(双指针),慢指针一次走一步,快指针一次走2步,这样快指针走完的时候慢指针刚好停在中间节点的位置上。当节点个数为偶数个的时候快指针走到尾,当节点个数为奇数个的时候快指针走到尾的下一个(NULL)。

代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* middleNode(struct ListNode* head)
{struct ListNode* slow, * fast;slow = fast = head;while (fast && fast->next){slow = slow->next;fast = fast->next->next;}return slow;
}

链表中倒数第k个节点

思路:

        有了上面那道题的启发,我们同样可以想出一个双指针的写法。既然要倒数第k个节点,那我让第一个指针先走k个接单,然后第二个指针再开始和第一个指针一起走不就行了。

从链表最后的NULL开始是倒数第0个,而第一个和第二个指针相差k个节点,也就是说,当第一个指针走到NULL,第二个指针再跟进一步,此时第二个指针指向的就是倒数第k个节点。

代码实现:(默认k是一个合理数字)

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* FindKthToTail(struct ListNode* pListHead, int k)
{struct ListNode* n1 = pListHead;struct ListNode* n2 = pListHead;int i = 0;while (n1){if (i >= k){n2 = n2->next;}n1 = n1->next;i++;}return n2;
}

合并两个有序链表

合并后:

思路:

        创建一个新链表,依次比较链表中的节点,每次取小的节点,尾插到新链表中即可。

代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{if (l1 == NULL)return l2;if (l2 == NULL)return l1;struct ListNode* head = NULL, * tail = NULL;//先取一个做第一个if (l1->val < l2->val){head = l1;tail = l1;l1 = l1->next;}else{head = l2;tail = l2;l2 = l2->next;}//从第二个开始往后接while (l1 && l2){if (l1->val < l2->val){tail->next = l1;tail = tail->next;l1 = l1->next;}else{tail->next = l2;tail = tail->next;l2 = l2->next;}}if (l1 == NULL){tail->next = l2;}else{tail->next = l1;}return head;
}

链表分割

思路:

        将原始链表分为两个,一个提出大于x的节点(big链表),一个提出小于x的节点(small链表),最后将small链表的最后一个节点连上big链表的第一个节点,返回small链表的第一个节点。

怎么把大于x/小于x的节点提出来呢?给一个cur指针遍历原始链表,然后将大于或者小于x的尾插到big链表/small链表中就行了。

本题还需要注意一点:

        记得把big链表尾插的最后一个节点的next置NULL,不然可能会出现死循环,如下图例子所示:

代码实现:

struct ListNode* partition(struct ListNode* pHead, int x)
{if (pHead == NULL){return NULL;}//创建哨兵位/头节点,方便尾插struct ListNode* bigHead = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* smallHead = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* big = bigHead;//沿着big链表往下走struct ListNode* small = smallHead;//沿着small链表往下走struct ListNode* cur = pHead;//沿着原始链表往下走while (cur)//遍历与尾插{if (cur->val < x){small->next = cur;cur = cur->next;small = small->next;}else{big->next = cur;cur = cur->next;big = big->next;}}//小于x的链表接上大于x的链表small->next = bigHead->next;//大于x的链表最后一个节点next置NULLbig->next = NULL;struct ListNode* returnHead = smallHead->next;//保存small链表第一个有效数据节点的地址free(bigHead);//释放头节点,防止内存泄漏free(smallHead);return returnHead;
}

链表的回文结构

思路:

        从中间节点开始,将后续节点逆置,然后前半部分和后半部分链表从头开始依次比较。由下图分析可见,当有偶数个节点时,逆置后1和1比,2和2比,最后都指向空。当有奇数个节点时,1和1比,2和2比,3和他自己比,最后都指向空,这种方法同样可以检测回文结构。

关于异或(有些人可能会想到异或):

        异或只能观察节点数据是否重复,不能观察节点排列顺序,也就不能检测回文结构了。

代码实现:(找中点节点和逆置链表搬运了上文的代码)

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* middleNode(struct ListNode* head)//找到中间位置的节点
{struct ListNode* slow, * fast;slow = fast = head;while (fast && fast->next){slow = slow->next;fast = fast->next->next;}return slow;
}struct ListNode* reverseList(struct ListNode* head)//链表逆置
{struct ListNode* cur = head;struct ListNode* next = NULL;struct ListNode* newHead = NULL;while (cur){next = cur->next;//头插cur->next = newHead;newHead = cur;//迭代往后走cur = next;}return newHead;
}int chakPalindrome(struct ListNode* A)//回文结构查询
{if (A == NULL)return 0;struct ListNode* mid = middleNode(A);struct ListNode* newHead = NULL;newHead = reverseList(mid);struct ListNode* oldcur = A;struct ListNode* newcur = newHead;while (oldcur && newcur){if (oldcur->val == newcur->val){oldcur = oldcur->next;newcur = newcur->next;}else{return 0;//False}}return 1;//True
}

相交链表

思路:

一:暴力解法。

        依次取A链表中每个界定啊跟B链表中所有节点比较。如果有地址相同的节点,就是相交,第一个相同的是交点。

二:利用长度差,让两个指针同时到达交点。

        先用双指针遍历一遍,当一个指针走到尾节点时开始计数,直到第二个指针走到尾节点,判断尾节点地址是否相等(即两链表是否相交),同时得到两个指针的相差节点个数k。若相交,则从头开始再遍历一遍,这次先让第二个指针走k个节点,再两个指针同时开始走,依次比较每个节点的地址,当地址相同时表示此节点为交点。

解法二代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* getIntersectionNode(struct ListNode* headA, struct LsitNode* headB)
{struct ListNode* curA = headA;struct ListNode* curB = headB;int distance = 0;//两个指针相差节点数int Afast = 0;//A更快走到尾节点标志位int Bfast = 0;//第一次遍历while (curA->next && curB->next){curA = curA->next;curB = curB->next;}if (curA->next == NULL){while (curB->next){curB = curB->next;distance++;}Afast = 1;}else{while (curA->next){curA = curA->next;distance++;}Bfast = 1;}if (curA != curB){return NULL;}//第二次遍历curA = headA;//重置cur指针curB = headB;if (Afast){while (distance--){curB = curB->next;}}else if (Bfast){while (distance--){curA = curA->next;}}while (curA != curB)//curA 与 curB相等时为交点{curA = curA->next;curB = curB->next;}return curA;
}

环形链表1:是否带环

经典的带环结构:

思路:

        首先确定带不带环:快慢指针

        slow和fast指向链表的开始,slow一次走一步,fast一次走两步,不带环,fast会走到NULL,带环,fast就会在环里面追上slow。

代码实现:

struct ListNode
{int val;struct ListNode* next;
};int hasCycle(struct ListNode* head)
{struct ListNode* slow = head, * fast = head;while (fast && fast->next){fast = fast->next->next;slow = slow->next;if (slow == fast){return 1;}}return 0;
}

带环延伸问题

1.为什么slow和fast一定会在环中相遇,会不会在环里面错过,永远遇不上?请证明一下。

结论:他们一定会相遇。

证明:

        第一步:fast一定先进环,slow走了入环前距离的一半。

        第二步:随着slow进环,fast已经在环里面走了一段,走了多少和环的大小有关系。

        假设slow进环的时候,slow和fast的距离是N(fast追上slow的距离),fast开始追slow,slow每次往前走1步,fast往前走2步,每追一次,判断一下相遇,每追一次,fast和slow之间的距离-1,N是整数倍的单位长度,而任何整数都是1的倍数,每次追一都判断,那么一定不会错过。

        也可以从距离N的角度来看:一开始距离N,追一次N-1,追两次N-2,然后N-3,... 4,3,2,1,0。当距离N为0时fast和slow相遇。

2.为什么slow走一步,fast走两步?能不能fast一次走3、4、5...n步呢?

结论:fast一次走n步,n>2不一定会相遇。

证明:

        根据上面的证明,我们可以通过fast追击slow的距离变化来看是否会错过。

        当fast与slow之间的追击距离为N时,假设fast一次走3步,slow一次走1步,他们之间的距离变化如下:

        如果C-1是奇数,那么就永远追不上了,如果C-1是偶数,那么追第二圈就可以追上了。

        根据上述分析,我们可以类推到fast一次走4步的情况,当N不是3的倍数时,N会追到-1或者-2,也就是说,当C-1或者C-2不是3的倍数时,就永远追不上了。

        也就是说,当slow一次走一步,fast一次走2步以上的步数时,都可能存在追不上的场景,所以才需要slow一次走一步,fast一次走两步。

环形链表2:环的入口点

思路一:

        slow走一步,fast走两步,一定会相遇,如何求环的入口点呢?

结论:

        一个指针从相遇点开始走,一个指针从链表头开始走,他们会在环的入口点相遇。

具体分析如下图所示:

代码实现:

struct ListNode
{int val;struct ListNode* next;
};struct ListNode* Findmeet(struct ListNode* head)//找到快慢指针相遇点
{struct ListNode* slow = head, * fast = head;while (fast && fast->next){fast = fast->next->next;slow = slow->next;if (slow == fast){return slow;}}return NULL;
}struct ListNode* detectCycle(struct ListNode* head)//链表环入口点
{struct ListNode* meet = Findmeet(head);if (meet == NULL)return NULL;struct ListNode* tailHead = head;struct ListNode* tailMeet = meet;while (tailHead != tailMeet){tailHead = tailHead->next;tailMeet = tailMeet->next;}return tailMeet;
}

思路二:

        将链表从meet节点断开,将这个断开节点后的链表看作一个相交链表。感到熟悉了吗?这就是上文有完整代码示例的相交链表。具体分析可见下图,代码在上文“相交链表”中。

复杂链表的复制

思路:

代码实现:

struct Node
{int val;struct Node* next;struct Node* random;
};struct Node* copyRandomList(struct Node* head)
{if (head == NULL)return NULL;struct Node* tail = head;struct Node* next = head->next;while (tail)//创建复制节点,复制val,将复制节点插入原链表中{struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->val = tail->val;//插入newNode节点tail->next = newNode;newNode->next = next;//迭代tail = next;if (next != NULL){next = next->next;}}struct Node* cur = head;while (cur)//遍历,处理复制节点random{if (cur->random){cur->next->random = cur->random->next;//根据原节点random,处理复制节点random}else//random为空{cur->next->random = NULL;}cur = cur->next->next;//迭代}tail = head;//重新将指针放回开头next = head->next->next;//原链表的第二个节点struct Node* newListHead = head->next;while (tail)//恢复原链表,链接新复制链表{if (next == NULL){tail->next->next = NULL;      //最后一个复制节点}else{tail->next->next = next->next;//复制节点的next指向后一个复制节点}tail->next = next;            //当前原链表节点指向下一个原链表节点、//迭代tail = next;if (next != NULL){next = next->next->next;//next指针指向下一个原链表节点}}return newListHead;
}


双向链表

        上文有说到,实际中最常用的两种结构:

1.无头单向非循环链表:

        结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

2.带头双向循环链表:

        结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。至于怎么简单,下文中双链表接口函数的实现会体现。

(注:下文中双向链表都指的是带头双向循环链表)

双向链表接口函数的实现

        双向链表,一般来说用来存放数据,所以需要了解基本的增删查改。

接口函数一览

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int LTDataType;typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;LTNode* ListInit();//创建哨兵位头节点
void ListPrint(LTNode* phead);//打印链表
LTNode* CreatListNode(LTDataType x);//创建一个新节点
void ListPushBack(LTNode* phead,LTDataType x);//尾插
void ListPopBack(LTNode* phead);//尾删
void ListPushFront(LTNode* phead, LTDataType x);//头插
void ListPopFront(LTNode* phead);//头删LTNode* ListFind(LTNode* phead, LTDataType x);//查找void ListInsert(LTNode* pos, LTDataType x);//在pos位置之前插入一个数据节点
void ListErase(LTNode* pos);//将pos位置的节点删除void ListDestroy(LTNode* phead);//销毁链表

创建哨兵位头节点与打印链表

LTNode* ListInit()//创建一个哨兵位头节点
{//哨兵位头节点LTNode* phead = (LTNode*)malloc(sizeof(LTNode));phead->next = phead;phead->prev = phead;return phead;
}void ListPrint(LTNode* phead)//打印链表
{assert(phead);LTNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}

创建一个新节点

LTNode* CreatListNode(LTDataType x)//创建一个新节点
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

数据节点插入

void ListInsert(LTNode* pos, LTDataType x)//在pos位置之前插入一个数据节点
{assert(pos);LTNode* newNode = CreatListNode(x);LTNode* prev = pos->prev;//链接上一个节点prev->next = newNode;newNode->prev = prev;//链接下一个节点newNode->next = pos;pos->prev = newNode;
}

数据节点删除

void ListErase(LTNode* pos)//将pos位置的节点删除
{assert(pos);LTNode* next = pos->next;LTNode* prev = pos->prev;//链接上一个与下一个节点next->prev = prev;prev->next = next;//释放节点空间free(pos);pos = NULL;
}

尾插与尾删

void ListPushBack(LTNode* phead, LTDataType x)//尾插
{assert(phead);LTNode* tail = phead->prev;LTNode* newnode = CreatListNode(x);//链接tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;//也可以复用Insert函数//ListInsert(phead, pos);
}void ListPopBack(LTNode* phead)//尾删
{assert(phead);//防止传入空指针assert(phead->next != phead);//防止哨兵位头节点被删除LTNode* oldEnd = phead->prev;LTNode* newEnd = oldEnd->prev;//删除尾节点free(oldEnd);oldEnd = NULL;//链接新的尾节点newEnd->next = phead;phead->prev = newEnd;//也可以复用Erase函数//ListErase(phead->prev);
}

头插与头删

void ListPushFront(LTNode* phead, LTDataType x)//头插(在哨兵位和第一个数据中间插入一个数据)
{assert(phead);//防止传入空指针LTNode* oldHeadData = phead->next;//旧的头数据节点LTNode* newHeadData = CreatListNode(x);//哨兵位链接新节点phead->next = newHeadData;newHeadData->prev = phead;//新节点链接旧头数据节点newHeadData->next = oldHeadData;oldHeadData->prev = newHeadData;//也可以复用Insert函数//ListInsert(phead->next, pos);
}void ListPopFront(LTNode* phead)//头删
{assert(phead);assert(phead->next != phead);//防止链表为空LTNode* oldHeadData = phead->next;LTNode* newHeadData = oldHeadData->next;//链接新头数据节点phead->next = newHeadData;newHeadData->prev = phead;//释放旧头数据节点空间free(oldHeadData);oldHeadData = NULL;//也可以复用Erase函数//ListErase(phead->next);
}

数据查找(返回pos指针)

LTNode* ListFind(LTNode* phead, LTDataType x)//查找
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x)return cur;elsecur = cur->next;}return NULL;
}

销毁链表

void ListDestroy(LTNode* phead)//销毁链表
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);phead = NULL;//这里并不能改变外面传进来的实参,需要在外面对phead置空
}


总结

链表与顺序表的优劣

链表:双向带头循环链表

优点:

        1、任意位置插入删除效率高。O(1)

        2、按需申请释放空间。

缺点:

        1、不支持随机访问。(用下标访问)意味着:一些排序,二分查找等在这种结构上不适用。

        2、链表存储一个值,同时要存储链接指针,也有一定消耗(一般不用在意)

        3、cpu高速缓存命中率更低(详情见下文额外拓展)

顺序表:

优点:(用下标访问)

        1、支持随机访问。需要随机访问结构支持算法可以很好的适用。

        2、cpu高速缓存命中率更高(详情见下文额外拓展)

缺点:

        1、头部中部插入删除时间效率低。O(N)

        2、连续的物理空间,空间不够了以后需要增容。

                a、增容有一定程度程度消耗。

                b、为了避免频繁增容,一般我们都按倍数去增,用不完可能存在一定的空间浪费

额外拓展

与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell


(全文完)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/60065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(微信小程序)基于Spring Boot的校园失物招领平台的设计与实现(vue3+uniapp+mysql)

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

【Linux课程学习】:Linux的调试器-gdb的使用(常见指令,cgdb)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux课程学习 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 安装cgdb&#xff1a; 基本环境 1检查gdb是否…

【WRF-Urban】URBPARM.TBL参数解释及内容

【WRF-Urban】URBPARM.TBL参数解释及内容 URBPARM.TBL参数解释URBPARM.TBL参考 参考WRF-Urban教程-Urban Canopy Model URBPARM.TBL参数解释 各个城市相应的城市参数差异很大&#xff08;例如&#xff0c;有些城市的道路可能很宽但建筑物很矮&#xff0c;而其他城市的道路可…

DBeaver 连接 OceanBase Oracle 租户

DBeaver 是一款通用的数据库工具软件&#xff0c;支持任何具有JDBC驱动程序的数据库。DBeaver 需要 Java 运行环境的支持。截稿时 DBeaver 24.0.0 版本默认提供的 OceanBase 驱动是连接 MySQL 的&#xff0c;想连接 Oracle 租户需要新建一个驱动器使用。 下载数据库驱动包 1、…

使用 Prompt API 与您的对象聊天

tl;dr&#xff1a;GET、PUT、PROMPT。现在&#xff0c;可以使用新的 PromptObject API 仅使用自然语言对存储在 MinIO 上的对象进行总结、交谈和提问。在本文中&#xff0c;我们将探讨这个新 API 的一些用例以及代码示例。 赋予动机&#xff1a; 对象存储和 S3 API 的无处不在…

23种设计模式-访问者(Visitor)设计模式

文章目录 一.什么是访问者模式&#xff1f;二.访问者模式的结构三.访问者模式的应用场景四.访问者模式的优缺点五.访问者模式的C实现六.访问者模式的JAVA实现七.代码解释八.总结 类图&#xff1a; 访问者设计模式类图 一.什么是访问者模式&#xff1f; 访问者模式&#xff08;…

【开源免费】基于Vue和SpringBoot的在线考试系统(附论文)

本文项目编号 T 624 &#xff0c;文末自助获取源码 \color{red}{T624&#xff0c;文末自助获取源码} T624&#xff0c;文末自助获取源码 网络的广泛应用给生活带来了十分的便利。所以把在线考试管理与现在网络相结合&#xff0c;利用java技术建设在线考试系统&#xff0c;实现…

【H2O2|全栈】MySQL的云端部署

目录 前言 开篇语 准备工作 MySQL移除 为什么需要移除&#xff1f; 移除操作 Yum仓库 yum简介 rpm安装 yum库安装 MySQL安装 使用yum安装 开机自启动 检查运行状态 MySQL配置 初始密码 ​编辑登录 修改root密码 退出MySQL 字符集配置 重启数据库 结束语 …

DAY30|贪心算法Part04|LeetCode:452. 用最少数量的箭引爆气球、435. 无重叠区间、763.划分字母区间

目录 LeetCode:452. 用最少数量的箭引爆气球 基本思路 C代码 LeetCode:435. 无重叠区间 基本思路 C代码 LeetCode:763.划分字母区间 基本思路 C代码 LeetCode:452. 用最少数量的箭引爆气球 力扣代码链接 文字讲解&#xff1a;LeetCode:452. 用最少数量的箭引爆气球 视…

Enterprise Architect 16 下载、安装与无限30天操作

文章目录 Enterprise Architect 16 简介&#xff08;一&#xff09;支持多种建模语言和标准&#xff08;二&#xff09;强大的版本控制、协作和文档管理功能&#xff08;三&#xff09;增强的技术和用户体验&#xff08;四&#xff09;高级功能和扩展性 一&#xff0c;下载软件…

31.3 XOR压缩和相关的prometheus源码解读

本节重点介绍 : xor 压缩value原理xor压缩过程讲解xor压缩prometheus源码解读xor 压缩效果 xor 压缩value原理 原理:时序数据库相邻点变化不大&#xff0c;采用异或压缩float64的前缀和后缀0个数 xor压缩过程讲解 第一个值使用原始点存储计算和前面的值的xor 如果XOR值为0&…

解决 IDEA 修改代码重启不生效的问题

前言 在使用 IntelliJ IDEA 进行 Java 项目开发时&#xff0c;有时会遇到一个令人头疼的问题&#xff1a;修改了代码后&#xff0c;重启服务却发现更改没有生效。通常情况下&#xff0c;解决这个问题需要通过 Maven 的 clean 和 compile 命令来强制重新编译&#xff0c;但这显…

java 根据 pdf 模板带图片文字生成pdf文件

在现代应用开发中,自动生成包含动态内容的 PDF 文档在电子发票、合同生成、表单填充等场景中有着广泛的应用。本文将介绍如何使用 iText 库动态填充 PDF 模板字段,并在指定位置插入签名和公章图片。 项目需求 假设我们有一个 PDF 模板文件,包含表单字段,如用户姓名、地址…

MTK主板定制_联发科主板_MTK8766/MTK8768/MTK8788安卓主板方案

主流市场上的MTK主板通常采用联发科的多种芯片平台&#xff0c;如MT8766、MT6765、MT6762、MT8768和MT8788等。这些芯片基于64位Cortex-A73/A53架构&#xff0c;提供四核或八核配置&#xff0c;主频可达2.1GHz&#xff0c;赋予设备卓越的计算与处理能力。芯片采用12纳米制程工艺…

Windows仿macOS?看这一篇就够了

如果你有任何关于Windows仿macOS的问题&#xff0c;可加入942644281 &#xff08;QQ群&#xff09; Date9.20更新&#xff1a;增加功能按键左移部分Date9.16更新&#xff1a;增加了大多数资源的网盘链接Date9.15更新&#xff1a;增加StartAllBack&#xff0c;资源管理器调整部…

无法下载element-admin的依赖无法运行

目录 无法下载element-admin的依赖偷懒方法完美方法 运行element-admin错误 之前在下载element-admin时遇到了多多少少的问题 无法下载element-admin的依赖 偷懒方法 开始是从github下载源码后安装依赖时在与tui-editor相关的依赖时就会卡死&#xff0c;然后报错&#xff0c…

基于Java Springboot幼儿园管理系统

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据…

新一代API开发工具,让API调试更快 更简单

新一代API开发工具 代理调试 请求测试一站式解决方案 Reqable Fiddler Charles Postman, 让API调试更快 &#x1f680; 更简单 &#x1f44c; 直接上下载地址 根据系统,下载对应的版本即可 https://reqable.com/zh-CN/download/

Python | Leetcode Python题解之第564题寻找最近的回文数

题目&#xff1a; 题解&#xff1a; class Solution:def nearestPalindromic(self, n: str) -> str:m len(n)candidates [10 ** (m - 1) - 1, 10 ** m 1]selfPrefix int(n[:(m 1) // 2])for x in range(selfPrefix - 1, selfPrefix 2):y x if m % 2 0 else x // 10…

爬虫——数据解析与提取

第二节&#xff1a;数据解析与提取 在网络爬虫开发中&#xff0c;获取网页内容&#xff08;HTML&#xff09;是第一步&#xff0c;但从这些内容中提取有用的数据&#xff0c;才是爬虫的核心部分。HTML文档通常结构复杂且充满冗余信息&#xff0c;因此我们需要使用高效的解析工…