深入理解数据结构(2):顺序表和链表详解

标头风景图片


  • 文章主题:顺序表和链表详解🌱
  • 所属专栏:深入理解数据结构📘
  • 作者简介:更新有关深入理解数据结构知识的博主一枚,记录分享自己对数据结构的深入解读。😄
  • 个人主页:[₽]的个人主页🔥🔥

顺序表和链表详解

  • 前言
  • 线性表
  • 顺序表
    • 概念及结构
    • 顺序表的实现
    • 顺序表的问题及思考
  • 链表
    • 链表的概念及结构
    • 链表的分类
    • 链表的实现
    • 链表是否有环的相关问题
    • 双向链表的实现
  • 顺序表和链表的区别
  • 结语

前言

顺序表和链表是数据结构的基础,也是最基本、最简单、最常用的数据结构——线性表的两种主要形式,以下是博主对于顺序表和链表这两种最基本数据结构的详解。


线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
线性表中的顺序表和链表


顺序表

概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可分为:
1.静态顺序表:使用定长数组存储元素
静态顺序表
2.动态顺序表:使用动态开辟的数组存储。
动态顺序表

顺序表的实现

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

SList.h

// SeqList.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>typedef int SLDataType;
typedef struct SeqList
{SLDataType* a;int size;     // 有效数据int capacity; // 空间容量
}SeqList;// 对数据的管理:增删查改 
void SeqListInit(SeqList* ps);
void SeqListDestroy(SeqList* ps);
static void SeqListCheckCapacity(SeqList* ps);
void SeqListPrint(SeqList* ps);
void SeqListPushBack(SeqList* ps, SLDataType x);
void SeqListPushFront(SeqList* ps, SLDataType x);
void SeqListPopBack(SeqList* ps);
void SeqListPopFront(SeqList* ps);// 顺序表查找
int SeqListFind(SeqList* ps, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SeqListInit(SeqList* ps)
{assert(ps);ps->a = NULL;ps->size = 0;ps->capacity = 0;
}
void SeqListDestroy(SeqList* ps)
{assert(ps);if (ps->a != NULL){free(ps->a);ps->a = NULL;ps->size = 0;ps->capacity = 0;}
}
void SeqListPrint(SeqList* ps)
{assert(ps);for (int i = 0; i < ps->size; i++){printf("%d ", ps->a[i]);}printf("\n");
}
static void SeqListCheckCapacity(SeqList* ps)
{assert(ps);if (ps->size == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newcapacity);// realloc函数在对NULL扩容时功能会自动if (tmp == NULL)                                                               // 变成类似于malloc函数的功能直接在动态{                                                                              // 区开辟一块动态内存空间,但仅限于是具有perror("realloc fail");                                                    // 了初始化空指针的函数才行。return;}ps->a = tmp;ps->capacity = newcapacity;}
}
void SeqListPushBack(SeqList* ps, SLDataType x)
{     assert(ps);SeqListCheckCapacity(ps);ps->a[ps->size++] = x; 
}
void SeqListPushFront(SeqList* ps, SLDataType x)
{assert(ps);SeqListCheckCapacity(ps);//挪动腾空int end = ps->size - 1;while (end >= 0){ps->a[end + 1] = ps->a[end];--end;}ps->a[0] = x;ps->size++;
}
void SeqListPopBack(SeqList* ps)
{assert(ps);//温柔的检查//if (ps->size == 0)//{//	printf("The size has already been set to 0.");//	return;//}//暴力检查assert(ps->size > 0);ps->size--;
}
void SeqListPopFront(SeqList* ps)
{assert(ps);assert(ps->size > 0);int begin = 1;//挪动覆盖while (begin < ps->size){ps->a[begin - 1] = ps->a[begin++];}ps->size--;
}
//void SeqListFind()
//{
//
//}
void SeqListInsert(SeqList* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size);SeqListCheckCapacity(ps);// 挪动腾空int end = ps->size - 1;while (end >= pos){ps->a[end + 1] = ps->a[end--];}ps->a[pos] = x;ps->size++;
}
void SeqListErase(SeqList* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos < ps->size);SeqListCheckCapacity(ps);int begin = pos + 1;while (begin < ps->size){ps->a[begin - 1] = ps->a[begin++];}ps->size--;
}
int SeqListFind(SeqList* ps, SLDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++){if (ps->a[i] == x)return i;}return -1;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList1()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);SeqListPushFront(&S1, 10);SeqListPushFront(&S1, 20);SeqListPushFront(&S1, 30);SeqListPushFront(&S1, 40);SeqListPrint(&S1);SeqListDestroy(&S1);
}
void TestSeqList2()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);SeqListPopBack(&S1);SeqListPopBack(&S1);SeqListPrint(&S1);SeqListDestroy(&S1);
}
void TestSeqList3()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListPopFront(&S1);SeqListPrint(&S1);SeqListDestroy(&S1);//SeqListPopFront(&S1);// 顺序表为空仍在删除导致运行时断言报断言错误(严格来说并不属于编译器报错的一种,//SeqListPrint(&S1);   // 是一种防止程序发生运行错误及时止损的方法,报断言错误后直接回去改断言错误即可。
}
void TestSeqList4()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);SeqListInsert(&S1, 2, 40);SeqListPrint(&S1);SeqListDestroy(&S1);
}
void TestSeqList5()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);SeqListErase(&S1, 2, 40);SeqListPrint(&S1);SeqListDestroy(&S1);
}
void TestSeqList6()
{SeqList S1;SeqListInit(&S1);SeqListPushBack(&S1, 1);SeqListPushBack(&S1, 2);SeqListPushBack(&S1, 3);SeqListPushBack(&S1, 4);SeqListPushBack(&S1, 5);SeqListPushBack(&S1, 6);SeqListPushBack(&S1, 7);SeqListPushBack(&S1, 8);SeqListPrint(&S1);int pos = SeqListFind(&S1, 2);if (pos != -1){SeqListErase(&S1, pos);}SeqListPrint(&S1);SeqListDestroy(&S1);
}
int main()
{TestSeqList6();return 0;
}

顺序表的问题及思考

问题

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

思考:如何解决以上问题呢?下面给出了链表的结构来看看。


链表

链表的概念及结构

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

链表的分类

实际中链表的结构非常多样,常见的有单向与双向、带头与不带头、循环与不循环,这几种常见情况组合起来就有8种链表结构:

  1. 单向或者双向单向或者双向

  2. 带头或者不带头带头或者不带头

  3. 循环或者不循环循环或者不循环
    虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构(其中后一种因为虽然节点与节点之间的逻辑结构上更复杂(本质实现起来也只是多个节点的区别,和单个节点实现后指前、前指后(循环的效果)上稍复杂一些,基本不会特别复杂),函数逻辑上实现却因为节点与节点之间的双向反倒会相比普通单链表(通常指无头单向不循环链表)起来更简单,时间复杂度上也会直接由O(N)到O(1)降阶)
    用得最多的无头单向非循环链表与带头双向循环链表
    (可直接简记成三种状况的对应前后两个状况的极端的链表用得最多,其中有的比不得用得更多,并且也更好些,时间上也会花更少。)

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为非单独使用的其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中单独使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

链表的实现

SList

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// slist.h
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SListNode;// 动态申请一个节点
static SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
// 答:因为单链表单向的性质在只给出了将要插入的位置的而不给出其前面的指针值的情况下无法用与单链表相反的逆向
// 找到该节点所对应的前一个节点的指针值的,一般此时会再给一个在其位置前的二级指针(原因是因为当单链表中一个节
// 点都没有时得开辟一个节点改变头指针的值(头指针没有对应的结构体使其解引用到动态内存中改变其值,所以只能通过指向指针变量的二级指针来正规的解引用来改变其值,节点中的结构体通过指针的嵌套本质上实现的就是另一种与二级指针类似的可以真正改变指针值的效果,只不过这种类型的指针本身就储存在它对应类型的结构体中才可以直接向外用一个同类型的一级指针采用结构体中的引用(用指针去引用本质就只一种解引用的方式)的访问到它内部的一个结构体成员而已。))才能实现这个效果
// 相当于这个结构体类型是这两个一级指针中的过渡层,让这两个一级指针通过结构体过渡引用的方式实现了一个一级指针指向结构体类型,再由结构体类型指向一个其内部同类型的一级指针等价于二级指针直接解引用得到一个一级指针的效果,结构体就是一种这么神奇的类型,别说一级指向一级,连与其等价的变量指向指针都能够实现,甚至一级指二级等(这都是结构体这种变量引用的性质,其中用对应这种结构体的指针去引用的方式本质就是一级指针的解引用,只不过指向的结构体中刚好可以编入任意
// 级数的指针类型成员而已。
void SListInsertAfter(SListNode* pos, SLTDataType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
// 答:还是相同的原因因为单链表只能单向访问的限制,只给一个位置作参数还是无法知道单链表之前的节点坐标,而如果删除的是pos位置的节点的话又必须知道pos位置之前的节点指针所对应的指向下一个成员的指针变量的位置从而改变其指针变量指向下一个节点的指针值才能够将链表接上的,所以只传一个指针变量的参数是无法做到删除对应节点后再找到上一个节点的衔接指针从而衔接上删去了一个指针的断开的链表的。
void SListEraseAfter(SListNode* pos);// 在pos的前面插入
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x);// 这些地方用了二级指针的原因是因为都会涉及到头指针内部数值的改变,因为单列表普通节点的数值的改变,直接让它的上一个节点指向的一级指针改变即可,是图纸就没有结构体给他直接用一级指针引用到他的头上去,所以只能通过普通的二级指针引用到一级的身上,才能切切实实改变一级指针,但是这个函数算法里面,所以就肯定得用一个二级指针的参数来改变这种头指针。
// 删除pos位置
void SLTErase(SListNode** pplist, SListNode* pos);
void SLTDestroy(SListNode** pplist);

SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListPrint(SListNode* plist)
{// 此处不用加空指针的断言,因为在单链表中空指针的效果指的就是一个数据都没有,如果加了断言就无法反映单链表中没有数据的情况了,// 顺序表加了空指针断言的原因是因为就算其数据为0时它所对应的结构体变量早创建好了(可理解成是进顺序表的那扇门),哪怕初始化// 也是将其指向真正储存的表中内容的指针变成空指针,而这扇门因为创建好了其指针值在数据个数为0的情况下也是不会变成0的。printf("phead");if (plist == NULL){printf("(NULL)->");}else{printf("->");SListNode* cur = plist;while (cur){printf("%d", cur->data);if (cur->next == NULL)printf("(NULL)->");elseprintf("->");cur = cur->next;}}printf("void\n");
}
static SListNode* BuySListNode(SLTDataType x)
{SListNode* pnewnode = (SListNode*)malloc(sizeof(SListNode));if (pnewnode == NULL){perror("malloc fail");exit(-1);}pnewnode->data = x;pnewnode->next = NULL;return pnewnode;
}
void SListPushBack(SListNode** pplist, SLTDataType x)
{assert(pplist);if (*pplist == NULL){*pplist = BuySListNode(x);// 是链表中没有一个变量时的正经情况,所以不会等于温柔的检查逻辑,因为此处是和后面等价的一种情况}                             // 而不是对一种错误参数的迅速止损逻辑,虽然两者很相似,但是还是不能等价,所以这种情况下没有去采// return;的格式去表示及时止损的特点,因为其不是对一个错误信息的检查站的特点,而是一个和后面情// 况等价的都有可能存在的逻辑,所以是用的非独立而是和后面一种情况等价的条件双分支语句的形式表示// 的。// 找尾else{SListNode* tail = *pplist;while (tail->next){tail = tail->next;}tail->next = BuySListNode(x);}
}
void SListPushFront(SListNode** pplist, SLTDataType x)
{assert(pplist);SListNode* pnewnode = BuySListNode(x);pnewnode->next = *pplist;*pplist = pnewnode;
}
void SListPopBack(SListNode** pplist)
{assert(pplist);// 1、零个节点assert(*pplist);// 2、一个节点if ((*pplist)->next == NULL){free(*pplist);*pplist = NULL;}// 多个节点else{SListNode* tail = *pplist, * prev = NULL;// 找尾while (tail->next){prev = tail;tail = tail->next;}free(tail);prev->next = NULL;}
}
void SListPopFront(SListNode** pplist)
{assert(pplist);assert(*pplist);SListNode* prev = *pplist;// 这里的prev理解的是删除节点后的之前位置的节点*pplist = prev->next;free(prev);
}
SListNode* SListFind(SListNode* plist, SLTDataType x)
{SListNode* cur = plist;while (cur && cur->data != x)// 没找到就返回NULL,且先写判断空指针的条件是利用了逻辑操作符&&可以控制求值顺序的特性,在空指针引用之前就可以先判断了空指针,再直接结束循环,从而在判断住第一个错误之后就退出循环改变两边操作目的求值顺序后,因为第一个操作目的结果为假直接跳过了后一个操作目的判断直接跳出了循环,防止后续判断下一节点的数据值时发生野指针访问错误的报错。{cur = cur->next;}return cur;
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{assert(pos);SListNode* pnewnode = BuySListNode(x);pnewnode->next = pos->next;pos->next = pnewnode;
}
void SListEraseAfter(SListNode* pos)
{// 1、零/一个节点assert(pos && pos->next);// 2、多个节点SListNode* erasednode = pos->next;pos->next = erasednode->next;free(erasednode);
}
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x)
{assert(pplist);// 严格限定pos一定是链表里的一个有效节点(即不会使其为指向链表末端非节点处的处于最后一个节点的防止其指针变量为野指针的空指针)//assert(*pplist);// 严格情况即用有长度情况的非尾插算法即可,即去除尾插分支的简单双分支//assert(pos);// 要么都是空,要么都不是空,要么头指针不为空,pos为空// 灵活的限定pos的位置————可以在头在尾插入assert((!pos && *pplist) || (!pos && !(*pplist)) || (pos && *pplist));// 头插(长度为零时可理解为长度为零时的尾插)(因为前面不是prev,而是一个头指针,所以只能用头指针那边的算法)if (*pplist == pos){SListPushFront(pplist, x);}// 有长度时的尾插(降低时间复杂度,本身也可用else中的逻辑处理)else if (!pos){SListPushFront(pplist, x);}// 有长度时的中间插入else{SListNode* prev = *pplist;while (prev->next != pos){prev = prev->next;}SListNode* pnewnode = BuySListNode(x);pnewnode->next = prev->next;prev->next = pnewnode;}
}
void SLTErase(SListNode** pplist, SListNode* pos)
{assert(pplist);assert(*pplist);assert(pos);// 1、头删(长度为一时可理解成尾删)(能头删尾删的积极采用,降低时间复杂度)if (*pplist == pos){SListPopFront(pplist);}// 2、长度大于一时更简单的利用前一个节点的逻辑进行的中间删除else{// 本质和头指针一样也是用改变链表中的指针值,只不过要借用节点才能找到,既然用节点就能找到也就没必要用二级指针了,逻辑上顺水推舟,无需画蛇添足的多增加几步了SListNode* prev = *pplist;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);}
}
void SLTDestroy(SListNode** pplist)
{assert(pplist);// 温柔的检查// 1、无节点if (*pplist == NULL){return;}// 2、有节点else{SListNode* cur = *pplist, * next = NULL;while (cur){next = cur->next;free(cur);cur = next;}// 小细节:销毁当前链表后指向当前链表的头指针赋回空指针,1、代表当前链表销毁后长度变回0。2、防止头指针变成野指针在后续被错误引用重新赋成空指针。*pplist = NULL;}
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListTest1()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest2()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListPushFront(&phead, 10);SListPrint(phead);SListPushFront(&phead, 20);SListPrint(phead);SListPushFront(&phead, 30);SListPrint(phead);SListPushFront(&phead, 40);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest3()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);//SListPopBack(&phead);//SListPrint(phead);SLTDestroy(&phead);
}
void SListTest4()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest5()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest6()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest7()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SListNode* pos = SListFind(phead, 6);if (pos == NULL)printf("没找到%d。\n", 6);elseprintf("找到了%d!\n", 6);pos = SListFind(phead, 5);;if (pos == NULL)printf("没找到%d。\n", 5);elseprintf("找到了%d!\n", 5);SLTDestroy(&phead);
}
void SListTest8()
{SListNode* phead = NULL;SListPushBack(&phead, 5);SListPrint(phead);SListNode* pos = SListFind(phead, 5);SListInsertAfter(pos, 90);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest9()
{SListNode* phead = NULL;SListPushBack(&phead, 5);SListPushBack(&phead, 6);SListPrint(phead);SListNode* pos = SListFind(phead, 5);SListEraseAfter(pos);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest10()
{SListNode* phead = NULL;SListPrint(phead);SListNode* pos = phead;SLTInsert(&phead, pos, 100);SLTInsert(&phead, NULL, 100);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest11()
{SListNode* phead = NULL;SListPushBack(&phead, 90);SListPrint(phead);SListNode* pos = SListFind(phead, 90);SLTErase(&phead, pos);SListPrint(phead);SLTDestroy(&phead);
}
void SListTest12()
{SListNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPushBack(&phead, 5);SListPrint(phead);SLTDestroy(&phead);if (SListFind(phead, 1) == NULL && phead == NULL)printf("已清空,且头指针不是野指针,赋回了空指针。\n");
}
int main()
{SListTest11();return 0;
}

链表是否有环的相关问题

给定一个链表,如何判断链表中是否有环呢?

思路:快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。
扩展问题

  • 为什么快指针每次走两步,慢指针走一步可以?
    假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度减一。
    此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈(一个循环内刚要相遇时快指针一次又会多走几步又超过慢指针又要重新追还不一定会相遇的情况)的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
  • 快指针一次走3步,4步,···n步行吗?
    与步数差的奇偶性,非环直链的长度,慢指针第一次入环相对于快指针在运动方向一边的相对位置的奇偶性(非环直链与整个圆环长度的关系共同决定,可由其两项推出),以及整个圆环的长度有关,无固定规律,需分具体情况讨论,只有快指针一次走2步时这一种情况才能保证不管什么情况都能在有环链表中一次就相遇,来判断是否有环。
  • 在快指针一次走两步的通用情况下会得出一个结论
    让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,且在环外指针刚好入环时,肯定会与环内指针在入环点处相遇(运用通过证明得出的该结论,可比在确定为环内位置的相交点将环剪断通过判断相交链表的相交点来判断入环点位置,更快更巧妙一些地解决带环链表判断入环点位置的问题(都先需要快慢指针确定环内位置的点,但后续前者只要两次O(N)的遍历,而后者需要四次,并且后者也为设置更多的变量的暴力拆解法))。
  • 证明
    确定圆环入环点的证明

双向链表的实现

List.h

#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;
}ListNode;// 创建返回链表的头结点
ListNode* ListCreate(LTDataType x);
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pHead, ListNode* pos);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
ListNode* ListCreate(LTDataType x)
{struct ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail");exit(-1);}// 创建自己构成循环的链表节点(在将其用于创建哨兵位时尤其见效(只有一个节点,必须自己指自己))newnode->_data = x;newnode->_next = newnode;newnode->_prev = newnode;return newnode;
}void ListDestory(ListNode* pHead)
{assert(pHead);ListNode* cur = pHead->_next;// 清理节点while (cur != pHead){ListNode* tmp = cur;cur = cur->_next;free(tmp);}// 清理哨兵位free(pHead);
}void ListPrint(ListNode* pHead)
{assert(pHead);printf("哨兵位<=>");ListNode* cur = pHead->_next;while (cur != pHead){printf("%d<=>", cur->_data);cur = cur->_next;}printf("\n");
}void ListPushBack(ListNode* pHead, LTDataType x)
{assert(pHead);// 独立版本:ListNode* newnode = ListCreate(x);ListNode* tail = pHead->_prev;//pHead                    tail   newnodetail->_next = newnode;newnode->_prev = tail;newnode->_next = pHead;pHead->_prev = newnode;// Insert替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同)://ListInsert(pHead, x);
}void ListPopBack(ListNode* pHead)
{assert(pHead);assert(pHead->_next != pHead);// 独立版本:ListNode* tail = pHead->_prev;ListNode* tailPrev = tail->_prev;//pHead                    tailprev   tailtailPrev->_next = pHead;pHead->_prev = tailPrev;free(tail);// Erase替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同)://ListErase(pHead, pHead->_prev);
}void ListPushFront(ListNode* pHead, LTDataType x)
{assert(pHead);// 独立版本:ListNode* newnode = ListCreate(x);ListNode* first = pHead->_next;// pHead   firstnewnode->_next = first;first->_prev = newnode;pHead->_next = newnode;newnode->_prev = pHead;// Insert替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同)://ListInsert(pHead->_next, x);
}void ListPopFront(ListNode* pHead)
{assert(pHead);assert(pHead->_next != pHead);// 独立版本:ListNode* first = pHead->_next;ListNode* second = first->_next;// pHead   first   secondpHead->_next = second;second->_prev = pHead;free(first);// Erase替换版本(在带头双向循环链表的结构中逻辑与该函数完全相同)://ListErase(pHead, pHead->_next);
}ListNode* ListFind(ListNode* pHead, LTDataType x)
{assert(pHead);ListNode* cur = pHead->_next;// 从第一个数据节点查找至最后一个while (cur != pHead){// 找到返回值if (cur->_data == x)return cur;cur = cur->_next;}// 未找到返回NULLreturn NULL;
}void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = ListCreate(x);ListNode* posPrev = pos->_prev;//pHead          posPrev   pos   ...newnode->_next = pos;pos->_prev = newnode;posPrev->_next = newnode;newnode->_prev = posPrev;
}void ListErase(ListNode* pHead, ListNode* pos)
{assert(pos);assert(pos != pHead);// 专门传一个头指针的参数,为了防止将链表的哨兵位给清理了,使链表的头指针变成野指针,从而直接导致整个链表的数据不能通过头指针当作钥匙给访问到具体内存,直接造成整个链表数据的内存泄漏ListNode* posNext = pos->_next;ListNode* posPrev = pos->_prev;// posPrev   pos   posNextposPrev->_next = posNext;posNext->_prev = posPrev;free(pos);
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"
void ListTest1()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 插入链表数据节点ListPushBack(pHead, 1);ListPushBack(pHead, 2);ListPushBack(pHead, 3);ListPushBack(pHead, 4);ListPushBack(pHead, 5);ListPrint(pHead);// 销毁链表ListDestory(pHead);pHead = NULL;
}void ListTest2()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 尾插链表数据节点ListPushBack(pHead, 1);ListPushBack(pHead, 2);ListPushBack(pHead, 3);ListPushBack(pHead, 4);ListPushBack(pHead, 5);ListPrint(pHead);// 尾删链表数据节点ListPopBack(pHead);ListPrint(pHead);ListPopBack(pHead);ListPrint(pHead);ListPopBack(pHead);ListPrint(pHead);ListPopBack(pHead);ListPrint(pHead);ListPopBack(pHead);ListPrint(pHead);//ListPopBack(pHead);//ListPrint(pHead);// 销毁链表ListDestory(pHead);pHead = NULL;
}void ListTest3()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 插入链表数据节点ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPrint(pHead);// 销毁链表ListDestory(pHead);pHead = NULL;
}void ListTest4()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 插入链表数据节点ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPrint(pHead);// 插入链表数据节点ListPopFront(pHead);ListPrint(pHead);ListPopFront(pHead);ListPrint(pHead);ListPopFront(pHead);ListPrint(pHead);ListPopFront(pHead);ListPrint(pHead);ListPopFront(pHead);ListPrint(pHead);//ListPopFront(pHead);//ListPrint(pHead);// 销毁链表ListDestory(pHead);pHead = NULL;
}void ListTest5()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 插入链表数据节点ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPrint(pHead);// 查找节点->插入节点ListNode* pos = ListFind(pHead, 3);ListInsert(pos, 30);ListPrint(pHead);// 销毁链表ListDestory(pHead);pHead = NULL;
}void ListTest6()
{// 初始化链表(创造链表的头节点(哨兵位)的同时将链表头节点的地址赋值给头指针)ListNode* pHead = ListCreate(-1);// 插入链表数据节点ListPushFront(pHead, 1);ListPushFront(pHead, 2);ListPushFront(pHead, 3);ListPushFront(pHead, 4);ListPushFront(pHead, 5);ListPrint(pHead);// 查找节点->删除节点LTDataType n = 3;printf("删除%d:\n", n);ListNode* pos = ListFind(pHead, n);if (pos == NULL)printf("没找到。\n");else{ListErase(pHead, pos);ListPrint(pHead);}n = 6;printf("删除%d:\n", n);pos = ListFind(pHead, n);if (pos == NULL)printf("没找到。\n");else{ListErase(pHead, pos);ListPrint(pHead);}// 销毁链表ListDestory(pHead);pHead = NULL;
}
int main()
{ListTest6();return 0;
}

顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持:O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)只需修改指针指向
容量动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储 + 频繁访问任意位置插入和删除频繁
缓存利用率

备注:缓存利用率参考存储体系结构 以及 局部原理性。
系统的存储层次结构


结语

以上就是博主对顺序表和链表的详解,😄希望对你的数据结构的学习有所帮助!看都看到这了,点个小小的赞或者关注一下吧(当然三连也可以~),你的支持就是博主更新最大的动力!让我们一起成长,共同进步!

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

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

相关文章

机器学习——降维算法-奇异值分解(SVD)

机器学习——降维算法-奇异值分解&#xff08;SVD&#xff09; 在机器学习中&#xff0c;降维是一种常见的数据预处理技术&#xff0c;用于减少数据集中特征的数量&#xff0c;同时保留数据集的主要信息。奇异值分解&#xff08;Singular Value Decomposition&#xff0c;简称…

csp资料

头文件 #include <bits/stdc.h> using namespace std isdigit(c); isalpha(c); switch(type){case value : 操作 } continue;//结束本轮循环 break;//结束所在的整个循环tips: //除法变乘法来算 //减法变加法 num1e42;//"1e4"表示10的4次方//用于移除容器中相…

某国投集团知识竞赛活动方案

一、抽签分组办法 1.抽签&#xff1a;参赛队伍赛前进行抽签分组。 2.分组&#xff1a;全部报名参赛队伍按照抽签顺序分为4组&#xff0c;每组7支队伍进行预赛&#xff0c;9月16日上午1、2组进行初赛&#xff0c;9月16日下午3、4组进行初赛。每组决出的前三名进入决赛。 二、初…

二维码门楼牌管理应用平台建设:引领现代化小区管理新篇章

文章目录 前言一、二维码门楼牌管理应用平台概述二、三维动态单体化技术的优势三、二维码门楼牌管理应用平台的应用场景四、展望未来 前言 随着城市化的快速推进&#xff0c;现代化小区如雨后春笋般涌现&#xff0c;对小区管理的效率和智能化提出了更高要求。二维码门楼牌管理…

鸿蒙OS开发实例:【瀑布流式图片浏览】

介绍 瀑布流式展示图片文字&#xff0c;在当前产品设计中已非常常见&#xff0c;本篇将介绍关于WaterFlow的图片浏览场景&#xff0c;顺便集成Video控件&#xff0c;以提高实践的趣味性 准备 请参照[官方指导]&#xff0c;创建一个Demo工程&#xff0c;选择Stage模型熟读Har…

图像分割论文阅读:Automatic Polyp Segmentation via Multi-scale Subtraction Network

这篇论文的主要内容是介绍了一种名为多尺度差值网络&#xff08;MSNet&#xff09;的自动息肉分割方法。 1&#xff0c;模型整体结构 整体结构包括编码器&#xff0c;解码器&#xff0c;编码器和解码器之间是多尺度差值模块模块&#xff08;MSM&#xff09;&#xff0c;以及一…

Vue3 使用 v-bind 动态绑定 CSS 样式

在 Vue3 中&#xff0c;可以通过 v-bind 动态绑定 CSS 样式。 语法格式&#xff1a; color: v-bind(数据); 基础使用&#xff1a; <template><h3 class"title">我是父组件</h3><button click"state !state">按钮</button>…

牛客NC31 第一个只出现一次的字符【simple map Java,Go,PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c 核心 Map参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可*…

INA350ABSIDDFR 仪表放大器 单路低功耗 TSOT-23-8

NA350ABSIDDFR 是一款高精度、低功耗、单片式精密运算放大器。它具有出色的直流精度和低失调电压&#xff0c;适用于需要高精度信号处理的应用。这款产品广泛应用于各种领域&#xff0c;如工业控制、医疗设备、测试与测量设备以及通信系统等。 制造商: Texas Instruments …

思维题,LeetCode331. 验证二叉树的前序序列化

一、题目 1、题目描述 序列化二叉树的一种方法是使用 前序遍历 。当我们遇到一个非空节点时&#xff0c;我们可以记录下这个节点的值。如果它是一个空节点&#xff0c;我们可以使用一个标记值记录&#xff0c;例如 #。 例如&#xff0c;上面的二叉树可以被序列化为字符串 &quo…

3.恒定乘积自动做市商算法及代码

中心化交易所的安全风险 在中心化交易所中注册账户时&#xff0c;是由交易所生成一个地址&#xff0c;用户可以向地址充币&#xff0c;充到地址之后交易所就会根据用户充币的数量显示在管理界面中。但是充币的地址是掌管在交易所之中的&#xff0c;资产的控制权还是在交易所。…

从0开始搭建基于VUE的前端项目(二) 安装和配置element-ui组件库

版本和地址 ElementUI 2.15.14 (https://element.eleme.io/)按需引入的插件 babel-plugin-component(1.1.1) https://github.com/ElementUI/babel-plugin-component安装 npm install element-ui完整引入(不建议) 这种方式最后打包的源文件很大,造成网络资源的浪费main.jsimpo…

MFC(二)集成基础控件

目录 OnCreateCStatic【标签&#xff0c;图片】CEdit【文本框&#xff0c;密码框&#xff0c;数值框&#xff0c;文本区】CButton【按钮&#xff0c;单选按钮&#xff0c;多选按钮】CComboBox【下拉列表&#xff0c;列表】CSliderCtrl【滑动条】CListCtrl【表格】CAnimateCtrl【…

C语言分支循环探秘:解锁编程逻辑的无限可能 篇章1

目录 1.if语句 2.关系操作符 3.条件操作符 4.逻辑操作符&#xff1a;&&&#xff0c;||&#xff0c;&#xff01; 5.switch语句 6.while循环 7.for循环 8.do-while循环 9.break和continue语句 10.循环的嵌套 11.goto 导入 C语言是结构化的程序设计语言&…

数字化服务升级:数字乡村改善农民生活质量

随着信息技术的迅猛发展&#xff0c;数字化浪潮已经深入社会的各个角落&#xff0c;为人们的生活带来了翻天覆地的变化。在乡村地区&#xff0c;数字化服务的升级正在逐步改变农民的生活方式&#xff0c;提高他们的生活质量。本文将围绕数字化服务升级&#xff0c;探讨数字乡村…

【蓝桥杯选拔赛真题51】C++百位上的数字 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解析

目录 C百位上的数字 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、推荐资料 C百位上的数字 第十四届蓝桥杯青少年创意编程大赛C选拔赛真题 一、题目要求 1、编程实现 给定一个三位数&#xff0c…

京东云免费服务器申请入口,2024年最新免费云主机

京东云服务器免费6月申请入口 jdyfwq.com 在京东云免费云主机申请页面&#xff0c;免费云服务器配置为云主机2核4G5M和轻量云主机2C2G可以申请免费使用&#xff0c;目前京东云免费云服务器申请时长从之前的6个月缩短到1个月&#xff0c;如下图&#xff1a; 京东云免费云主机 云…

【Java】MyBatis快速入门及详解

文章目录 1. MyBatis概述2. MyBatis快速入门2.1 创建项目2.2 添加依赖2.3 数据准备2.4 编写代码2.4.1 编写核心配置文件2.4.2 编写SQL映射文件2.4.3 编写Java代码 3. Mapper代理开发4. MyBatis核心配置文件5. 案例练习5.1 数据准备5.2 查询数据5.2.1 查询所有数据5.2.2 查询单条…

Cisco ISR 4000 Series IOS XE Release IOSXE-17.13.1a ED

Cisco ISR 4000 Series IOS XE Release IOSXE-17.13.1a ED 思科 4000 系列集成服务路由器系统软件 请访问原文链接&#xff1a;https://sysin.org/blog/cisco-isr-4000/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 无耻抄…

Redis数据结构的基础插入操作

数据结构与内部编码 Redis常见的数据结构 数据结构和内部编码 数据结构的插入操作 在Redis中&#xff0c;数据结构的插入操作取决于你要插入的数据类型。以下是一些常见的数据结构和它们的插入操作&#xff1a; 字符串 (String)&#xff1a;使用 SET 命令来插入字符串。例…