【数据结构】链表解析与实战运用(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.xdnf.cn/news/20102.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Python练习31

Python日常练习 题目&#xff1a; 分别输入两个整数以及一个加减乘除中的算术运算符&#xff0c;输出运算结果&#xff0c; 若输入其它运算符&#xff0c;则退出程序; 例如&#xff1a; 输出格式如下 【输入一个整数&#xff1a;】1 【输入另一个整数&#xff1a;】2 …

uniapp 自定义加载组件,全屏加载,局部加载 (微信小程序)

效果图 全屏加载 页面加载使用 局部加载 列表加载里面使用 使用gif html <template><view><view class"" v-if"typeFullScreen"><view class"loading" v-if"show"><view class""><i…

Mac 修改默认jdk版本

当前会话生效 这里演示将 Java 17 版本降低到 Java 8 查看已安装的 Java 版本&#xff1a; 在终端&#xff08;Terminal&#xff09;中运行以下命令&#xff0c;查看已安装的 Java 版本列表 /usr/libexec/java_home -V设置默认 Java 版本&#xff1a; 找到 Java 8 的安装路…

动态规划-最长公共子序列

题目 最长公共子序列 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符的相对顺序的情况下删除某些字符&#xff08;也可以不删除任何字符&#xff0…

nvm安装node遇到的若干问题(vscode找不到npm文件、环境变量配置混乱、npm安装包到D盘)

问题一&#xff1a;安装完nvm后需要做哪些环境变量的配置&#xff1f; 1.打开nvm文件夹下的setting文件&#xff0c;设置nvm路径和安装node路径&#xff0c;并添加镜像。 root: D:\software\nvm-node\nvm path: D:\software\nvm-node\nodejs node_mirror: https://npmmirror.c…

Zookeeper的简单使用Centos环境下

目录 前言 一、ZOokeeper是什么&#xff1f; 二、安装Zookeeper 1.进入官网下载 2.解压到服务器 3.配置文件 三.使用Zookeeper 3.1启动相关指令 3.2其他指令 3.3ACL权限 总结 前言 记录下安装zookeeper的一次经历 一、ZOokeeper是什么&#xff1f; ZooKeeper是一…

疫情中的图书馆管理:Spring Boot系统设计

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了疫情下图书馆管理系统的开发全过程。通过分析疫情下图书馆管理系统管理的不足&#xff0c;创建了一个计算机管理疫情下图书馆管理系统的方案。文章介绍了疫情下图…

Excel数据动态获取与映射

处理代码 动态映射 动态读取 excel 中的数据&#xff0c;并通过 json 配置 指定对应列的值映射到模板中的什么字段上 private void GetFreightFeeByExcel(string filePath) {// 文件名需要以快递公司命名 便于映射查询string fileName Path.GetFileNameWithoutExtension(fi…

网络(TCP)

目录 TCP socket API 详解 bind(): 我们的程序中对myaddr参数是这样初始化的: listen(): accept(): 理解accecpt的返回值: 饭店拉客例子 connect tcp服务器和udp类似的部分代码 把套接字设置为监听状态&#xff08;listen&#xff09; 测试 查看端口号和IP地址&…

了解鱼叉式网络钓鱼攻击的社会工程学元素

一提到网络攻击&#xff0c;你可能会想象一个老练的黑客躲在类似《黑客帝国》的屏幕后面&#xff0c;利用自己的技术实力积极入侵网络。然而&#xff0c;许多攻击的现实情况远比这平凡得多。 一封带有“未送达”等无害主题的简单电子邮件被放在员工的垃圾邮件文件夹中。他们心…

TS流详解

目录 TS流结构 PSI 节目关联表&#xff08;PAT Program Association Table&#xff09; 条件接收表&#xff08;CAT Conditional Access Table&#xff09; 节目映射表&#xff08;PMT Program Map Table&#xff09; 网络信息表&#xff08;NIT Nerwork Information Tabl…

【图像处理识别】数据集合集!

本文将为您介绍经典、热门的数据集&#xff0c;希望对您在选择适合的数据集时有所帮助。 1 CNN-ImageProc-Robotics 机器人 更新时间&#xff1a;2024-07-29 访问地址: GitHub 描述&#xff1a; 通过 CNN 和图像处理进行机器人对象识别项目侧重于集成最先进的深度学习技术和…

C语言--分支循环编程题目

第一道题目&#xff1a; #include <stdio.h>int main() {//分析&#xff1a;//1.连续读取int a 0;int b 0;int c 0;while (scanf("%d %d %d\n", &a, &b, &c) ! EOF){//2.对三角形的判断//a b c 等边三角形 其中两个相等 等腰三角形 其余情…

Windows系统使用全功能的跨平台开源音乐服务器Navidrome搭建在线音乐库

文章目录 前言1. 安装Docker2. Docker镜像源添加方法3. 创建并启动Navidrome容器4. 公网远程访问本地Navidrome4.1 内网穿透工具安装4.2 创建远程连接公网地址4.3 使用固定公网地址远程访问 前言 在数字时代&#xff0c;拥有一个个性化、便捷的音乐库成为了许多人的需求。本文…

监控易DEMO功能深度解析:运维行业的智能化转型新助力

在数字化转型的浪潮中&#xff0c;运维行业正面临着前所未有的变革与挑战。为了应对日益复杂的IT架构和不断提升的运维需求&#xff0c;监控易的集中式跨平台一体化监控软件不断升级优化&#xff0c;以适应新的运维环境。本文将对监控易DEMO的功能进行深度解析&#xff0c;探讨…

ElasticSearch学习篇17_《检索技术核心20讲》最邻近检索-局部敏感哈希、乘积量化PQ思路

目录 场景在搜索引擎和推荐引擎中&#xff0c;对相似文章去重是一个非常重要的环节&#xff0c;另外是拍照识花、摇一摇搜歌等场景都可以使用它快速检索。 基于敏感性哈希的检索更擅长处理字面上的相似而不是语义上的相似。 向量空间模型ANN检索加速思路 局部敏感哈希编码 随…

SpringBoot MySQL的增删改查

创建数据库和表 安装MySQL&#xff1a;https://blog.csdn.net/qq_59636442/article/details/141926105 通过Navicat 创建数据库test 创建表student后随便添加一条数据 CREATE TABLE student (id int NOT NULL AUTO_INCREMENT,name varchar(255) DEFAULT NULL,email varchar(2…

23种设计模式-备忘录(Memento)设计模式

文章目录 一.什么是备忘录设计模式&#xff1f;二.备忘录模式的特点三.备忘录模式的结构四.备忘录模式的优缺点五.备忘录模式的 C 实现六.备忘录模式的 Java 实现七.总结 类图&#xff1a; 备忘录设计模式类图 一.什么是备忘录设计模式&#xff1f; 备忘录设计模式&#xff08…

如何删除pdf里的任意一页?删除PDF里任意一页的几种方法

如何删除pdf里的任意一页&#xff1f;尽管PDF文件具有许多优点&#xff0c;如跨平台兼容性和格式保真性&#xff0c;但在编辑和修改方面&#xff0c;它与像Word或Excel这类文档格式不同&#xff0c;通常不能像其他文档那样轻松进行直接的内容删除或修改。这让很多人以为&#x…

css3新特性(二十六课)

1、css3盒子模型 box - sizing: content - box&#xff1b; 是 CSS 中用于定义盒模型宽度和高度计算方式的一个属性值。在这种盒模型下&#xff0c;元素的宽度和高度&#xff08;width和height属性&#xff09;仅包括内容区域&#xff08;content&#xff09;的大小&#xff…