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


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

顺序表和链表详解

前言

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


线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。


顺序表

概念及结构

顺序表是用一段物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

顺序表一般可分为:

1.静态顺序表:使用定长数组存储元素

2.动态顺序表:使用动态开辟的数组存储。

顺序表的实现

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

SList.h

c 复制代码
// 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

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

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

c 复制代码
#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

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)->");
			else
				printf("->");
			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

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);
	else
		printf("找到了%d!\n", 6);

	pos = SListFind(phead, 5);;
	if (pos == NULL)
		printf("没找到%d。\n", 5);
	else
		printf("找到了%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

c 复制代码
#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

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   newnode
	tail->_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   tail
	tailPrev->_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   first
	newnode->_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   second
	pHead->_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;
	}
	// 未找到返回NULL
	return 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   posNext
	posPrev->_next = posNext;
	posNext->_prev = posPrev;
	free(pos);
}

Test.c

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) 只需修改指针指向
容量 动态顺序表,空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储 + 频繁访问 任意位置插入和删除频繁
缓存利用率

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


结语

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

相关推荐
梅茜Mercy1 小时前
数据结构:链表(经典算法例题)详解
数据结构·链表
青春男大1 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
Zer0_on1 小时前
数据结构栈和队列
c语言·开发语言·数据结构
一只小bit1 小时前
数据结构之栈,队列,树
c语言·开发语言·数据结构·c++
马浩同学2 小时前
【GD32】从零开始学GD32单片机 | DAC数模转换器 + 三角波输出例程
c语言·单片机·嵌入式硬件·mcu
我要学编程(ಥ_ಥ)2 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
一个没有本领的人2 小时前
win11+matlab2021a配置C-COT
c语言·开发语言·matlab·目标跟踪
一只自律的鸡2 小时前
C项目 天天酷跑(下篇)
c语言·开发语言
长安——归故李3 小时前
【C语言】成绩等级制
c语言·开发语言
追逐时光者3 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio