线性表详细讲述(带图)

news2024/12/23 15:05:52

文章目录

  • 线性表---顺序表和链表
  • 1.线性表
  • 2.顺序表
    • 2.1概念
    • 2.2 静态顺序表与动态顺序表
    • 2.3接口的实现
      • 2.3.1顺表的初始化
      • 2.3.2扩容
      • 2.3.3顺序表尾插
      • 2.3.4顺序表的尾删
      • 2.3.5顺序表的头插
      • 2.3.6顺序表的头删
      • 2.3.7顺序表的查找
      • 2.3.8顺序表的任意位置插入
      • 2.3.9顺序表的任意位置删除
      • 2.3.10顺序表的打印
      • 2.3.11顺序表的销毁
    • 2.4顺序表的总结
  • 3.顺序表的问题与思考
  • 4.链表
    • 4.1链表的概念
    • 4.2链表的结构
    • 4.3链表的分类
      • 4.3.1单向和双向
      • 4.3.2带头不带头
      • 4.3.3循环和非循环
  • 5. 单项不带头不循环的链表实现
    • 5.1接口
    • 5.2接口的实现
      • 5.2.1 购买节点
      • 5.2.2单链表的尾插
      • 5.2.3 单链表的尾删
      • 5.2.4单链表的头插
      • 5.2.5单链表的头删
      • 5.2.6单链表的查找
      • 5.2.7 单链表的任意位置插入
      • 5.2.8单链表任意位置的删除
      • 5.2.9单链表的打印
      • 5.2.10单链表的销毁
    • 5.3单链表的总结
  • 6双向带头循环链表的实现
    • 6.1接口
    • 6.2 接口的实现
      • 6.2.1购买节点
      • 6.2.2双向链表的初始化
      • 6.2.3双向链表的尾插
      • 6.2.4双向链表的尾删
      • 6.2.5双向链表的头插
      • 6.2.6双向链表的头删
      • 6.2.7双向链表的查找
      • 6.2.8双向链表的任意位置插入
      • 6.2.9双向链表的任意位置删除
      • 6.2.10双向链表的打印
      • 6.2.11双向链表的销毁
    • 6.3双向带头循环链表总结
  • 7.链表与顺序表的区别

线性表—顺序表和链表

1.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,

线性表在物理上存储时,通常以数组和链式结构的形式存储

请添加图片描述

2.顺序表

2.1概念

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

2.2 静态顺序表与动态顺序表

静态顺序表

在这里插入图片描述

这样定义的顺序表是静态的,也就是在程序运行时他所开辟的空间就是固定的,不可以在改变,只有改变代码中的MAX才可以改变数组大小。

这种方法:直接将空间写“死”,并不是一个好的方法。

好方法在下面

动态顺序表

在这里插入图片描述

这便是动态顺序表,当表满了之后会自动扩容(扩容函数实现)

相对于静态顺序表,它的空间在程序运行期间,并不是固定的,而是随着数据的增多而扩大表的容量。

2.3接口的实现

这里是对动态顺序表的实现。

typedef int SLTDataType;
typedef struct SeqList
{
	SLTDataType *arr;
	int sz;       //当前表中数据个数
	int capacity; //当前表的最大容量
}SeqList;

//顺序表的初始化
void SeqListInit(SeqList* sl);
//扩容函数
void Capacity(SeqList* sl);
//顺序表的尾插
void SeqListPushBack(SeqList* sl, SLTDataType x);
//顺序表的尾删
void SeqListPopBack(SeqList* sl);
//顺序表的头插
void SeqListPushFront(SeqList* sl, SLTDataType x);
//顺序表的头删
void SeqListPopFront(SeqList* sl);
//顺序表的查找
int SeqListFind(SeqList* sl, SLTDataType x);
//顺序表的任意位置插入
void SeqListInsert(SeqList* sl,int pos, SLTDataType x);
//顺序表的任意位置删除
void SeqListErase(SeqList* sl, int pos);
//顺序表的打印
void SeqListPrint(SeqList* sl);
//顺序表的销毁
void SeqListDestroy(SeqList* sl);

这些就是顺序表的基本功能

2.3.1顺表的初始化

//顺序表的初始化
void SeqListInit(SeqList* sl)
{
	assert(sl); //判断sl是否为NULL

	sl->arr = NULL;  // 初始化可以开辟空间也可以不开辟,这个是不开辟的
	//sl->arr = (SLTDataType*)malloc(sizeof(SLTDataType)*4);   这个是开辟空间的
	sl->sz = 0;

	sl->capacity = 0;
	//sl->capacity = 4    开辟空间了,容量就要初始化
}

2.3.2扩容

//扩容函数
void Capacity(SeqList* sl)
{
	assert(sl);

	//判断容量是否为0
	//如果是0的话使容量变为4
	//如果不为0的话使容量扩大2倍
	sl->capacity = (sl->capacity == 0 ? 4 : sl->capacity * 2);

	//通过realloc函数进行扩容
	SLTDataType* tmp = (SLTDataType*)realloc(sl->arr,sl->capacity * sizeof(SLTDataType));
	if (tmp == NULL)//判断是否开辟成功
	{
		//开辟不成功
		perror("Capacity::realloc:");
		exit(-1);
	}
	//开辟成功,将地址赋给sl->arr
	sl->arr = tmp;
}

2.3.3顺序表尾插

在这里插入图片描述

//顺序表的尾插
void SeqListPushBack(SeqList* sl, SLTDataType x)
{
	assert(sl); 

	//判断顺序表容量是否够
	//如果空间不够就扩容
	if (sl->capacity == sl->sz)
	{
		Capacity(sl);
	}

	//空间够了就开始写入数据
	sl->arr[sl->sz] = x;
	//写入后使sz++
	sl->sz++;
}

2.3.4顺序表的尾删

在这里插入图片描述

//顺序表的尾删
void SeqListPopBack(SeqList* sl)
{
	assert(sl);
	
	//如果顺序表中没有数据就不能再删除
	assert(sl->sz);

	//如果有数据开始删除
	sl->sz--;
}

有人肯定觉得删除数据后要将顺序表删除位置的数据置为0

sl->arr[sz-1] = 0

sl->sz--

其实写这一步的意义不打,你下次再次写的时候你直接就将原来数据覆盖了,再者说,如果你删除的数据要是0的话,你将它置为0不就多次一举

置与不置都可以,不会产生任何影响,我觉得没有必要取多写那一步。如果你有强迫症的话,你可以写那一步,反正都不影响结果,代码自己看着舒服就好😄

2.3.5顺序表的头插

在这里插入图片描述

//顺序表的头插
void SeqListPushFront(SeqList* sl, SLTDataType x)
{
	assert(sl);

	//先检查容量
	if (sl->capacity == sl->sz)
	{
		Capacity(sl);
	}

	//头插
	//需要将数据依次向后移,把第一个位置空下来取放入数据

	int i = sl->sz;
	while (i)
	{
		sl->arr[i] = sl->arr[i - 1];
		i--;
	}
    
	sl->arr[0] = x;
	//同时由于插入了数据,sz++
	sl->sz++;
}

2.3.6顺序表的头删

在这里插入图片描述

//顺序表的头删
void SeqListPopFront(SeqList* sl)
{
	assert(sl);
	//判断顺序表中是否有数据
	assert(sl->sz);

	//头删就是将后面的数据依次向前移,将第一个元素给覆盖了
	int i = 0;
	while (i < sl->sz - 1)
	{
		sl->arr[i] = sl->arr[i + 1];
		i++;
	}

	//最后将sz--
	sl->sz--;
}

2.3.7顺序表的查找

遍历元素,依次与要查找的数据进行对比

//顺序表的查找
int SeqListFind(SeqList* sl, SLTDataType x)
{
	assert(sl);

	//这里我写的是空表的话就不查找,直接报错,错误使用
	assert(sl->sz);

	//通过循环将顺序表中的元素依次对比。
	int i = 0;
	while (i < sl->sz)
	{
		if (sl->arr[i] == x)
			return i;
        i++;
	}

	//如果没有找到数据,就返回-1
	//为什么是-1呢?
	//其实只要是个负数都可以,一般用-1
	//0和正数是不行的,因为可能是数组的下标。
	return -1;
}

2.3.8顺序表的任意位置插入

任意位置插入和头插基本思路一样,依次向后移动,只不过不是插在第一个,而是任意的位置,但是这个位置要合理

//顺序表的任意位置插入
void SeqListInsert(SeqList* sl, int pos, SLTDataType x)
{
	assert(sl);
    //检查插入位置是否合理
	assert(pos>=0 && pos<=sl->sz);
	//先检查容量
	if (sl->capacity == sl->sz)
	{
		Capacity(sl);
	}

	//将pos位置(含pos位置)依次向后移
	//把pos位置空出来,将数据插入
	//这里的pos对应这数组下标
	int i = sl->sz;
	while (i > pos)
	{
		sl->arr[i] = sl->arr[i - 1];
		i--;
	}
	//在pos位置插入数据
	sl->arr[pos] = x;
	
	//最后将sz++
	sl->sz++;
}

2.3.9顺序表的任意位置删除

任意位置删除,和头删的方法一样,将要删除的数据后面的数据依次向前移动,将其覆盖,只不过是将pos位置覆盖,而不是第一个数据,思路是相同的。

//顺序表的任意位置删除
void SeqListErase(SeqList* sl, int pos)
{
	assert(sl);
	//检查顺序表是否为空
	assert(sl->sz);
    //检查插入位置是否合理
	assert(pos>=0 && pos<=sl->sz);

	//将pos后面的数据(不含pos)依次向前移
	int i = pos;
	while (i < sl->sz - 1)
	{
		sl->arr[i] = sl->arr[i + 1];
		i++;
	}

	//最后将sz--
	sl->sz--;
}

2.3.10顺序表的打印

//顺序表的打印
void SeqListPrint(SeqList* sl)
{
	assert(sl);

	for (int i = 0; i < sl->sz; i++)
	{
		printf("%d ", sl->arr[i]);
	}
	printf("\n");
}

2.3.11顺序表的销毁

//顺序表的销毁
void SeqListDestroy(SeqList* sl)
{
	assert(sl);

	//将之前动态开辟的空间是否,再将其置为NULL,避免野指针
	//sz与capacity置为0
	free(sl->arr);
	sl->arr = NULL;
	sl->capacity = 0;
	sl->sz = 0;
}

2.4顺序表的总结

看完这些,想必你们也大概知道顺序表了

我来总结一下吧:

只要是写入数据,就要判断容量是否够用,并且sz++

只要删除数据,就要判断表中是否有数据,并且sz–

尾插、尾删时间复杂度O(1);

头插头删时间复杂度为O(N);

随机插入与删除时间复杂度为O(1);

3.顺序表的问题与思考

问题:

1.顺序表的中间和头部的插入删除,时间复杂度为O(N)

2.异地扩容(不懂可以去了解一下realloc函数)要进行数据拷贝,释放旧空间。会有不小的消耗

3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间 。

思考:

有没有什么可以解决这些问题呢?

有的,那就是我们下面需要讲的链表,他完美的解决了上面的这些问题。

疑问:

既然链表这么好,直接学链表不就好了,学顺序表有什么意义?

首先我要否定这种观点,链表有优点同时也有它的缺点,而这些缺点,顺序表就可以完美的解决,他们各有各的用处,学到后面就知道了。

4.链表

4.1链表的概念

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

4.2链表的结构

逻辑上的结构

在这里插入图片描述

链表的逻辑图就是这样,一个结构连着一个结构,感觉他们是一个接着一个。其实并不是这样的

物理结构

在这里插入图片描述

着就是它的物理结构,实际在计算机中,这些数据并不一定是连着存储的,他们分散在堆区,靠next指针可以将他们连起来。

4.3链表的分类

4.3.1单向和双向

单向
在这里插入图片描述

双向

在这里插入图片描述

4.3.2带头不带头

带头

在这里插入图片描述

不带头

在这里插入图片描述

4.3.3循环和非循环

在这里插入图片描述

上面说了这么多中链表结构,他们互相组合共有八种:

  • 单向不带头不循环
  • 单项不带头循环
  • 单向带头不循环
  • 单向带头循环
  • 双向不带头不循环
  • 双向不带头循环
  • 双向带头不循环
  • 双向带头循环

说了这么多,大家肯定觉得害怕了吧。不用怕,其实学会一种其他都差不多。

我给大家讲两种:

1.单项不带头不循环

2.双向带头循环

有人就会说:这两种肯定差距很大吧,一看就是第一种特别简单,第二种特别难。

这么说也对。

第一种是结构最简单,但是实现是最复杂的

第二种是结构最难,实现最简单的

只要好好把这两种结构学好,其他结构就是手到擒来了。话不多说了,开始吧。

5. 单项不带头不循环的链表实现

5.1接口

// 1、无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x);// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表的任意位置插入
// 在pos位置之前插入
void SListInsert(SListNode** pplist,SListNode* pos, SLTDateType x);
// 单链表的任意位置删除
void SListEarse(SListNode** pplist, SListNode* pos);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表的销毁
void SListDestroy(SListNode** pplist);

5.2接口的实现

5.2.1 购买节点

// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	//判断是都从堆区申请到节点
	if (newnode == NULL)
	{
		perror("BuySListNode::malloc:");
		exit(-1);
	}

	//申请到的话
	newnode->next = NULL;
	newnode->data = x;

	return newnode;
}

5.2.2单链表的尾插

先画图理解理解

在这里插入图片描述

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);

	//获得新的节点
	SListNode* newnode = BuySListNode(x);

	//插入到现在的链表中
	//先判断链表中是否有数据
	//如果没有数据,不光要插入,还要将头指针*plist的值给改了
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		//先通过遍历找到尾,在将新节点给连上
		SListNode* tail = NULL;
		SListNode* cur = *pplist;
		//找尾
		while (cur)
		{
			tail = cur;
			cur = cur->next;
		}
		//链接
		tail->next = newnode;
	}

}

肯定有人疑问,为什么是二级指针呢?

因为,在main函数中,定义的是一个指针变量,要改变一级指针的内容,变要用二级指针了。

在这里插入图片描述

肯定有人问,为啥要定义一个指针变量,定义一个普通的结构体变量不行吗?

在某些情况下是不行的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

如果是结构体指针变量,他会将所有节点都存放到堆区,不会在栈区存放链表的节点,所以你返回后链表是可以正常使用的。

1、为什么,顺序表可以是普通变量呢,而不是指针?

顺序表它通过普通变量就可以将所有数据都存放的堆区,用指针变量会变得复杂。

2、链表就不能不用二级指针吗?好烦啊,不喜欢。

当然是可以的,只要你有办法将所有节点都存放到堆区就可以了。

通过带头的单链表,就可以。

3、既然可以为啥不说那个呢?说这个麻烦的,肯定没有那个好用

并不是这样的。在后面有些数据结构中,我们是要通过不带头的单链表完成的,所以还是要好好学不带头的单链表的,这个学会了,你的链表基本问题就不大了,加油😄。

5.2.3 单链表的尾删

画图

在这里插入图片描述

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	//要删除数据,得要现有数据吧,我们在这里断言一下
	assert(*pplist);

	//有数据
	//找到尾将他删除,并且将他的前一个的next置空
	//但是如果只有一个数据呢,又要改变头指针,将他置为NULL
	//分情况讨论

	//只有一个数据
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		//定义两个指针,一个是cur去变量找到尾,另一个是prev表示尾的前一个
		SListNode* cur = *pplist;
		SListNode* prev = NULL;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;
		}

		//将倒数第二个也就是prev置为NULL
		prev->next = NULL;
		//释放掉最后一个
		free(cur);
	}
}

5.2.4单链表的头插

画图:
在这里插入图片描述

在这里插入图片描述

// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);

	//购买一个节点
	SListNode* newnode = BuySListNode(x);

	//如果没有节点呢,如果只有一个节点呢,如果有多个节点呢?
	//写之前先去画图想想
	//这里都不影响

	//记录第一个节点
	//尽管没有数据也不影响,画图看看吧
	SListNode* front = *pplist;
	//将新节点插入,成为第一个节点
	*pplist = newnode;
	//将原来的第一个节点链接在新的节点后面
	newnode->next = front;
}

5.2.5单链表的头删

这里我就不画图,和上面的差不多,你们可以去画画看

// 单链表头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//先要判断链表是否为空
	assert(*pplist);

	//不为NULL
	//同样要想想只有一个数据的情况是不是需要特殊处理
	//这里是不需要的

	//记录第一个节点
	SListNode* front = *pplist;
	//记录第二个节点
	SListNode* second = front->next;
	//删除掉第一个节点,并且将第二个节点变为第一个
	*pplist = second;
	free(front);
}

1、为什么要定义这么多指针呢?

为了方便写程序,如果不定义多个指针,在实现有些功能上,你就需要考虑程序执行的顺序了,而定义多个指针就需要去管顺序问题。

例如:如果你不定义second这个指针,你就需要先将头指向第二个节点,再释放第一个节点

5.2.6单链表的查找

从前到后一个一个与要查找的数据进行比较,如果相同就返回这个数据的节点,如果找完都没有找到就返回NULL

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	//如果没有数据,那么我们就不查找了
	assert(plist);

	//循环查找
	SListNode* cur = plist;
	//这里的循环结束条件是cur,而不是cur->next,可以画图理解一下
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}

	//找完都没有找到,就但会NULL
	return NULL;
	
}

1、为什么这里传的是一级指针呢?

首先你需要理解什么时候需要传二级指针,只有当我们需要去更改首节点的地址时才需要去传入二级指针,

只要不更改首节点地址,就可以不需要二级指针,而查找函数,它并不会去改,所以一级就够用了。

5.2.7 单链表的任意位置插入

在这里插入图片描述

如果只有一个数据的话就相当于头插,需要更改头指针,单独处理

// 单链表的任意位置插入
//在pos位置之前插入
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	assert(pplist);

	SListNode* newnode = BuySListNode(x);
	//只有一个数据
	if (*pplist == pos)
	{
		*pplist = newnode;
		newnode->next = pos;
	}
	else
	{
        //找到pos位置之前的节点
		SListNode* cur = *pplist;
		SListNode* prev = NULL;
		while (cur != pos)
		{
			prev = cur;
			cur = cur->next;
		}

		//此时的cur指向的时pos前面的节点
		prev->next = newnode;
		newnode->next = pos;
	}
}

1、为什么你这里的插入时的pos是一个节点呢,而不是一个数字代表第几个。你这个节点我怎么传啊。

首先说一下,你要传数字,也是可以的,自己改改就可以了(要判断出入的数字是否合法)

这里的节点是通过SListFind查找后得到的节点,在这个节点之前插入,所以是先查找再插入。

5.2.8单链表任意位置的删除

找到pos前一个位置,pos后一个位置,删除pos位置

想的不是很明白的话,就自己画图看看,画图真的很有用。

如果只有一个数据要单独处理

// 单链表的任意位置删除
void SListEarse(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	//先判断是否数据
	assert(*pplist);

	//有数据
	//记录pos前面的位置,再记录pos位置后面的位置
	//删除pos位置

	//如果只有一个数据
	if (*pplist == pos)
	{
		*pplist = NULL;
		free(pos);
	}
	else
	{
		//pos之前的位置

		SListNode* cur = *pplist;
		SListNode* prev = NULL;
		while (cur != pos)
		{
			prev = cur;
			cur = cur->next;
		}

		//pos之后的数据
		SListNode* next = pos->next;

		//将pos之前与pos之后链接
		prev->next = next;
		//删除pos位置
		free(pos);
	}

}

5.2.9单链表的打印

// 单链表打印
void SListPrint(SListNode* plist)
{
	//循环打印
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}

	printf("NULL");
}

5.2.10单链表的销毁

在这里插入图片描述

// 单链表的销毁
void SListDestroy(SListNode** pplist)
{
	assert(pplist);

	//从头一个一个节点的释放
	SListNode* cur = *pplist;
	while (cur)
	{
		SListNode* prev = cur;
		cur = cur->next;
		free(prev);
	}
	//将头结点置为NULL
	*pplist = NULL;
}

5.3单链表的总结

  • 如果要插入数据,要传入二级指针
  • 如果要删除数据,先要判断链表中是否有数据,同样也要传入二级指针
  • 如果要改变头指针则要使用二级指针,如果不用修改头指针便不需要传入二级指针
  • 它的头插、头删时间复杂度位O(1);
  • 它的尾插、尾删、任意位置删除、任意位置插入时间复杂度为O(N);

6双向带头循环链表的实现

6.1接口

// 2、带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
    //指向后面的指针
	struct ListNode* next;
    //指向前面的指针
	struct ListNode* prev;
}ListNode;
//购买节点
ListNode* BuyListNode(LTDataType x);
//初始化双向链表
void ListInit(ListNode** pplist);
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的结点
void ListErase(ListNode* pos);

6.2 接口的实现

6.2.1购买节点

//购买节点
ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("BuyListNode::malloc:");
		exit(-1);
	}

	//创建成功

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	
	return newnode;
}

这里和单链表一样,只是多了一个指向前面的指针

6.2.2双向链表的初始化

去创建一个哨兵位的头,这个头没有实际一样,不去存储数据,只是为了当头
在这里插入图片描述

这里要用二级指针,和单链表一样,要改变头指针。

//初始化双向链表
void ListInit(ListNode** pplist)
{
	assert(pplist);

	//初始化双向链表就是创建一个头,这个头不关注它存储的数据,只是让他去做队列的头,会使程序简单好多
	//所以这里传入-1,你也可以随意写入
	ListNode* newnode = BuyListNode(-1);

	//这里让他指向自己,相当于构成了循环,也是为了方便后面
	newnode->next = newnode;
	newnode->prev = newnode;

	*pplist = newnode;

}

6.2.3双向链表的尾插

空表:

在这里插入图片描述

多个数据:

在这里插入图片描述

// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x)
{
    assert(plist);
    
	ListNode* newnode = BuyListNode(x);

	//找到尾
	ListNode* tail = plist->prev;

	//将新的节点链接到尾后
	//新的节点的prev指针指向tail
	tail->next = newnode;
	newnode->prev = tail;
	//将头指针的prev 指向 新的尾
	//新的尾的next指向头
	plist->prev = newnode;
	newnode->next = plist;	
}

6.2.4双向链表的尾删

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

// 双向链表尾删
void ListPopBack(ListNode* plist)
{
     assert(plist);
    
	//判断链表中是否没有数据,如果没有数据就不能删除
	assert(plist->next != plist);

	//有数据
	//找到倒数第一个节点与倒数第二节点
	ListNode* tail = plist->prev;
	ListNode* tailprev = tail->prev;

	//删除最后一个节点
	//头指向倒数第二个
	//倒数第二个指向头
	plist->prev = tailprev;
	tailprev->next = plist;
	free(tail);
}

6.2.5双向链表的头插

没有数据:

在这里插入图片描述

有数据:

在这里插入图片描述

// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x)
{
     assert(plist);
    
	ListNode* newnode = BuyListNode(x);

	//记录第一个节点
	ListNode* front = plist->next;

	//将他们链接
	plist->next = newnode;
	newnode->prev = plist;
	newnode->next = front;
	front->prev = newnode;
}

6.2.6双向链表的头删

在这里插入图片描述

拥有多个节点方法也是一样的,可以画图理解理解

// 双向链表头删
void ListPopFront(ListNode* plist)
{
     assert(plist);
    
	//判断链表中是否有数据
	assert(plist->next != plist);

	//有数据
	//记录第一个节点,与第二个节点
	ListNode* front = plist->next;
	ListNode* second = front->next;

	//将头结点与第二个链接,并且删除第一个节点
	plist->next = second;
	free(front);
}

6.2.7双向链表的查找

从头节点后的第一个节点开始遍历查找,知道再次到达头结点,查找完毕,没有找到就返回NULl

// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x)
{
	ListNode* cur = plist->next;

	while (cur != plist)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}

	return NULL;
}

6.2.8双向链表的任意位置插入

更据pos位置的这个节点,可以找到它前面的节点,通过prev指针,而不用重新遍历查找

将新的节点插入。

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
     assert(plist);
    
	ListNode* newnode = BuyListNode(x);

	//找到pos之前的节点
	ListNode* prev = pos->prev;

	//链接
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

6.2.9双向链表的任意位置删除

找到pos前面的节点通过prev指针,找到pos之后的节点,通过next指针。

将pos节点释放,将pos之前的节点与pos之后的节点链接

// 双向链表删除pos位置的结点
void ListErase(ListNode* pos)
{
     assert(plist);
    
	//找到pos之前的节点,找到pos之后的节点
	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

	//删除pos,并且链接pos之前的节点与pos之后的节点
	prev->next = next;
	next->prev = prev;

	free(pos);
}

6.2.10双向链表的打印

// 双向链表打印
void ListPrint(ListNode* plist)
{
     assert(plist);
	ListNode* cur = plist->next;
	while (cur != plist)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

6.2.11双向链表的销毁

从头节点之后的第一个节点开始逐个释放,当再次回到头结点,就代表除头结点之外的节点已经释放完毕,最后将头结点释放,再将他置为NULL。

这里要传二级指针,因为要改变头指针。

// 双向链表销毁
void ListDestory(ListNode** pplist)
{
     assert(pplist);
    
	ListNode* cur = (*pplist)->next;
	while (cur != *pplist)
	{
		ListNode* prev = cur;
		cur = cur->next;
		free(prev);
	}
	free(*pplist);
	*pplist = NULL;
}

6.3双向带头循环链表总结

双向链表虽然结构上比单项链表复杂,但是它的操作简单,不要去考虑是否要更改头指针的情况,所有情况同意i处理,所以写代码会比单链表简单太多,只要你可以理解双向链表的结构,写起来就是轻轻松松。

初始化,和销毁要传入二级指针,因为要改变头指针的内容

删除时要判读是否有数据。

它的头删、尾删、头插、尾插、任意位置的删除、任意位置的插入,时间复杂度都为O(1)。

7.链表与顺序表的区别

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

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

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

相关文章

[附源码]java毕业设计民宿网站管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【DL】linux服务器上安装Anaconda3

1.本地连接远程服务器 使用MobaXterm连接远程服务器 2.下载Anaconda3安装包 安装包下载地址 https://www.anaconda.com/ 因为我们要在linux服务器上安装,因此选择linux安装包 下载完成以后,将安装包拖进服务器 3.安装Anaconda3 打开终端,输入以下命令,目的是赋权限 c…

Vue路由

参考文献&#xff1a;Vue中的路由 一、路由理解&#xff1a; 一个路由就是一组映射关系&#xff08;key&#xff0c;value&#xff09;&#xff0c;多个路由需要路由器&#xff08;router&#xff09;进行管理。其中key是路径&#xff0c;value是组件。作用&#xff1a;设定访…

【C++笔试强训】第二十六天

&#x1f387;C笔试强训 博客主页&#xff1a;一起去看日落吗分享博主的C刷题日常&#xff0c;大家一起学习博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a;夜色难免微凉&#xff0c;前方必有曙光 &#x1f31e;。 &#x1f4a6;&a…

springboot security 集成 cas 问题 No subject alternative names present

springboot security 集成 cas 问题 No subject alternative names present前言一、问题1.实际问题二、大海捞针1.星星之火2.通用方法啰嗦一句解决2.新建三个类配置文件修改前言 场景&#xff1a; 在一次springboot security 集成 cas开发中&#xff0c;代码报错&#xff1a;j…

轻松掌握 jQuery 基础

文章目录&#x1f496; 前言&#x1f496; jQuery简介&#x1f496; jQuery安装及使用&#x1f496; jQuery的$&#x1f49e; 选择器&#x1f49e; 功能函数前缀&#x1f49e; window.onload&#x1f49e; 创建DOM元素&#x1f496; 投票快捷键&#x1f496; 前言 随着JavaScri…

手机怎么把图片转换成Word?这个小妙招大家要学会

使用手机怎么把图片转换成Word文档呢&#xff1f;大家在使用图片文件来办公的时候&#xff0c;有的图片里面有成段成段的文字&#xff0c;不仅阅读起来很不方便&#xff0c;还很难修改内容&#xff0c;这时候我们可以将图片里的内容转换成Word文档&#xff0c;就可以解决这一问…

利用gdal把多张tif合成一张大图

目录gdalwarpgdalbuildvrt有时候从网站下载遥感影像时&#xff0c;因为选定区域的遥感影像太大&#xff0c;下载后往往是自动就给切片下载了。特别是当选定区域特别大时&#xff0c;最后形成的切片会有几十甚至上百小块&#xff0c;且这些小块都没有重叠的地方&#xff0c;虽然…

MySQL8.0优化 - 事务的ACID特性

文章目录学习资料事务事务的ACID特性原子性&#xff08;atomicity&#xff09;一致性&#xff08;consistency&#xff09;隔离性&#xff08;isolation&#xff09;持久性总结学习资料 【MySQL数据库教程天花板&#xff0c;mysql安装到mysql高级&#xff0c;强&#xff01;硬…

以梦为马,不负韶华|电巢科技延安大学飞鹰计划实习班精彩回顾

时光流淌无声&#xff0c;昨天仿佛还初次见面&#xff0c;今天却又是一年的尾声。你是否结交到亲密的小伙伴&#xff1f;你是否感受到团队合作的魅力&#xff1f;你是否在延大这片沃土得到成长&#xff1f;假如你还没答案&#xff0c;那么看看其他人的回答。 在延安大学&#x…

node深度打印对象

Node 深度打印对象 在node中打印对象类型时&#xff0c;如果对象的层级过深&#xff0c;就会出现这样的问题 要想显示里面的值&#xff0c;就要通过JSON.stringify方法 这样打印出来不易于阅读且不美观&#xff0c;可以设置参数的方式来格式化JSON console.log(JSON.string…

【计算机组成原理】第三章单元测试

1.单选(2分) ‎执行算术右移指令的操作过程是 ‍A.操作数的符号位不变&#xff0c;各位顺次右移1位&#xff0c;符号位拷贝至最高数据位 B.操作数的符号位填0&#xff0c;各位顺次右移1位 C.操作数的符号位填1&#xff0c;各位顺次右移1位 D.进位标志移至符号位&#xff0…

九、Vue3基础之九

文章目录一、可视化1.1 接口API 也是后端项目1.2 前端项目开始1.2.1 echarts二、Vue3 Router2.1 Router的初步应用2.2 路由模式、Router原理2.3 命名路由、编程式导航2.4 历史记录2.5 路由传参2.6 嵌套路由2.7 命名视图2.8 重定向、别名2.9 导航守卫&#xff08;前置守卫&#…

SpringBoot整合dubbo(三)

整合nacos作为注册中心 一、下载nacos&#xff1a;Release 2.2.0-BETA (Oct 28, 2022) alibaba/nacos GitHub 1.1、直接启动时报错&#xff0c;需要指定单例启动&#xff1a;startup.cmd -m standalone 启动后在http://localhost:8848/nacos/index.html访问&#xff0c;默认…

Revit中记忆快速修改未识别的梁及“快速生成过梁”

一、Revit中记忆快速修改未识别的梁 我们在使用红瓦建模大师对结构梁进行翻模时&#xff0c;往往会出现梁未识别的情况(如图 1)&#xff0c;这需要我们人工手动去修改。图中这一跨梁的命名应该同 KL5 (2B)&#xff0c;只是尺寸不同&#xff0c;只需要将它替换成前一跨梁然后复制…

计算机网络—概述

互联网 互联网又称国际网络&#xff0c;指的是网络与网络之间所串连成的庞大网络&#xff0c;这些网络以一组通用的协议相连&#xff0c;形成逻辑上的单一巨大国际网络。简单的说&#xff0c;网络把主机连接起来&#xff0c;互联网就是把多种不同的网络连接起来。 ISP 互联…

分布式天花板?阿里百万架构师的ZK+Dubbo笔记,颠覆认知

蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#xff0c;…

Java语言知识大盘点(期末复习)一

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…

卡方检验简介

Chi square test&#xff08;卡方检验&#xff09;是用于评价两类变量之间是否存在相关性的统计检验方法。 医疗研究会产生大量不同类型的数据&#xff0c;最容易识别的是定量的数据。例如&#xff0c;直腿抬高 (SLR) 的受试者能够将腿抬高大于 0 度&#xff0c;这让我们可以计…

[附源码]Python计算机毕业设计JAVA疫情社区管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…