草庐IT

带头双向循环链表及链表总结

乄北城以北乀 2023-04-20 原文

1、链表种类大全

1、链表严格来说可能用2*2*2=8种结构,从是否带头,是否循环,是否双向三个角度区分。

2、无头单向循环链表一般不会在实际运用中直接存储数据,而会作为某些更复杂结构的一个子结构,毕竟它只在头插、头删时具有效率上的优势。

3、带哨兵卫的头有利于解决尾插时多种讨论的复杂情况。

双向有利于insert、erase的实现,这两个函数涉及到对pos位置结点的前一个结点的操作,而双向链表由于存放了prev指针,可以轻松找到前一个结点(不用为了找前一个结点而再次遍历),从而完成相关功能。

循环有利于直接通过phead->prev找到tail,不用遍历,提高尾部操作的效率。

2、接口函数

//打印
void LTPrint(LTNode* phead);
//初始化
LTNode* LTInit();
//销毁
void LTDestroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead,LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPustFront(LTNode* phead,LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//查找链表中某个值的位置
LTNode* LTFind(LTNode* phead,LTDataType x);
//pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
//pos位置删除
void LTErase(LTNode* pos);

结点的创建

typedef int LTDataType;

typedef struct LTNode
{
	struct LTNode* prev;
	struct LTNode* next;
	LTDataType data;

}LTNode;

与之前相同,以int类型作为链表存储的数据类型。

3、初始化

在之前的无头单向非循环链表中,我们用一个plist指针指向链表,创建时只需要将plist置空即可,操作简单,不需要通过函数实现。

在顺序表的实现中,要考虑到sz(顺序表当前存储的数据个数),capacity(顺序表当前容量),还有一个指向顺序表的初始指针(避免使用数组导致的一些缺点),过程较复杂需要通过函数。

在带头双向循环链表中,由于需要创建一个哨兵卫的头结点,且为循环结构,next,prev都指向tail,无元素时tail为自己,可以通过init函数来实现。 

//初始化
LTNode* LTInit()
{
	//初始化时创建一个哨兵卫头结点
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (NULL == phead)
	{
		perror("malloc fail");
		return NULL;
	}
	phead->next = phead->prev = phead;
	phead->data = -1;

	return phead;

}

phead的data暂且设置一个-1,其它值也可以。

4、打印

//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("<head>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

phead一定不为NULL,因此可以assert(phead)防止使用者误传入NULL。

phead头结点不为实际值,因此从phead->next开始遍历,cur只要不为phead的都要进入循环完成打印。

5、尾部操作

1、创建新结点

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (NULL == newnode)
	{
		perror("malloc fail");
		return NULL;
	}

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

}

插入操作前,需要malloc一个新结点。为防止野指针,在创建过程中就将其next和prev置为空

2、判断链表是否为空

删除操作前,需要判断链表是否为空。

//判断链表原来是否为空的函数
bool IsEmpty(LTNode* phead)
{
	return phead->next == phead;
}

这里采用bool值,返回值为true(非0),和false(0)两种,需要包含stdbool.h头文件,这里的phead->next如果等于phead,即链表中只有哨兵卫头结点,即为空,返回为1(非0),代表链表的状态为空

3、尾插

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	if (NULL == newnode)
	{
		perror("BuyLTNode fail");
		return;
	}

	//按照之前的思路,尾插需要先找尾
	// 这里phead的prev直接找尾   需要4步指针操作
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;

}

先利用phead->prev找到原来的尾结点tail,然后创建新的尾结点,随后通过4步指针操作建立newnode与tail和phead的联系。(不用遍历找尾,提高了效率)

 

 

带哨兵卫的头结点的优势,不用讨论原链表为空的情况。 

4、尾删

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	//先判断原来是否为空
	assert(!IsEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* prev = tail->prev;
	free(tail);
	tail = NULL;
	phead->prev = prev;
	prev->next = phead; 


}

assert内为!IsEmpty,即为空时报错。

分别用tail和prev标记要删除的尾结点以及前一个结点,free掉tail,利用两个指针建立prev与phead的联系,使其成为新尾。

 

 

 多删除时assert报错

6、头部操作

1、头插

//头插
void LTPustFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyLTNode(x);
	if (NULL == newnode)
	{
		perror("BuyLTNode fail");
		return;
	}

	newnode->next = phead->next;
	phead->next->prev = newnode;
	newnode->prev = phead;
	phead->next = newnode;

}

通过4个指针建立newnode与phead、phead->next的联系,要注意顺序,先建立与phead->next

的,否则其会被改变无法找到。

当然,在开始时新建立一个next指针指向phead->next的结点也可以。

 

2、头删


//头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(!IsEmpty(phead));
	LTNode* first = phead->next;

	phead->next = first->next;
	first->next->prev = phead;
	free(first);
	first = NULL;

}

先利用first标记要删除的头结点,然后用两个指针建立phead与first的next结点之间的关系,最后free掉first。

头部操作本身就是链表的优势,因此仍不需要遍历

 7、查找及指定位置pos操作

1、查找函数

//查找链表中某个值的位置
LTNode* LTFind(LTNode* phead,LTDataType x)
{
	assert(phead);
	assert(!IsEmpty(phead));

	LTNode* pos = phead->next;

	while (pos != phead)
	{
		if (pos->data == x)
		{
			return pos;
		}
		pos = pos->next;
	}
	printf("没找到\n");
	exit(0);
}

从phead->next的位置开始遍历,找到了则用pos返回该结点的地址,使用者可以用一个ret指针在外部接收,找不到则终止程序。查找前先确认链表不为空。

通过调试观察到,ret拿到了值为3的结点的地址 

还可以继续展开,观察带头双向循环链表的内部结构,是怎样完成循环的。 

2、insert指定位置前插入

//pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyLTNode(x);
	LTNode* prevptr = pos->prev;
	prevptr->next = newnode;
	newnode->next = pos;
	newnode->prev = prevptr;
	pos->prev = newnode;

}

先用prevptr找到pos的前一个结点,然后利用4个指针建立newnode与这两个结点的关系,完成中间位置插入。

可以用pos->prev指针直接找到前一个结点,不需要查找外的额外遍历,提高了效率。 

 

3、erase指定位置删除

//pos位置删除
void LTErase(LTNode* pos)
{
	assert(pos);

	//LTNode* prevpos = pos->prev;
	//LTNode* nextpos = pos->next;

	//prevpos->next = nextpos;
	//nextpos->prev = prevpos;

	//free(pos);
	//pos = NULL;

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

同样找到pos前一个位置的结点,连接pos前一个与后一个结点,然后free掉pos

 

4、优化头尾操作

只要有了insert和erase两个函数,就可以轻易完成头尾的4个操作函数,只需要复用上述两个即可

例如在pos前插入一个节点,实际效果为尾插。

insert(pos->next)效果为头插

erase(phead->next)为头删

erase(phead->prev)为尾删

 

8、链表的销毁

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	printf("销毁成功\n");
}

从phead->next位置开始遍历销毁即可,最后使用者在外部对创建的链表手动置空

 掌握了以上操作,20分钟写一个链表不是难事

目录

1、链表种类大全

2、接口函数

3、初始化

4、打印

5、尾部操作

1、创建新结点

2、判断链表是否为空

3、尾插

4、尾删

6、头部操作

1、头插

2、头删

 7、查找及指定位置pos操作

1、查找函数

2、insert指定位置前插入

3、erase指定位置删除

4、优化头尾操作

8、链表的销毁


有关带头双向循环链表及链表总结的更多相关文章

  1. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  2. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  5. ruby - Ruby 中的闭包和 for 循环 - 2

    我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对

  6. Ruby:数组中的下一个/上一个值,循环数组,数组位置 - 2

    假设我有一个没有特定顺序的随机数数组。假设这些是参加马拉松比赛的人的ID#,他们按照完成的顺序添加到数组中,例如:race1=[8,102,67,58,91,16,27]race2=[51,31,7,15,99,58,22]这是一个简化且有些做作的示例,但我认为它传达了基本思想。现在有几个问题:首先,我如何获得特定条目之前和之后的ID?假设我正在查看运行者58,我想知道谁在他之前和之后完成了比赛。race1,runner58:previousfinisher=67,nextfinisher=91race2,runner58:previousfinisher=99,nextfinishe

  7. ruby - 奇怪的 ruby​​ for 循环行为(为什么这样做有效) - 2

    defreverse(ary)result=[]forresult[0,0]inaryendresultendassert_equal["baz","bar","foo"],reverse(["foo","bar","baz"])这行得通,我想了解原因。有什么解释吗? 最佳答案 如果我使用each而不是for/in重写它,它看起来像这样:defreverse(ary)result=[]#forresult[0,0]inaryary.eachdo|item|result[0,0]=itemendresultendforainb基本上就

  8. ruby - 如何证明 Ruby `for` 循环实际上是使用 `each` 方法实现的? - 2

    在EloquentRuby(第21页,第一版,第六次打印)一书中,作者(RussOlsen)提倡使用each方法而不是for循环,这与我在其他地方读到的所有内容一致。但是作者还继续说,这样做的一个原因是for循环实际上调用了each方法,所以为什么不直接删掉中间人并使用each?所以我想知道这实际上是如何工作的。为了调查,我确实在github上的Ruby存储库上进行了搜索,但发现很难确定我在哪里/如何看到它的实际效果。重述问题:我如何证明Rubyfor循环实际上是使用each方法实现的? 最佳答案 您可以通过编写一个实现每个的类来展

  9. ruby - 循环遍历数组的元素 - 2

    我想从0到2循环@a:0,1,2,0,1,2。defset_aif@a==2@a=0else@a=@a+1endend也许有更好的方法? 最佳答案 (0..2).cycle(3){|x|putsx}#=>0,1,2,0,1,2,0,1,2item=[0,1,2].cycle.eachitem.next#=>0item.next#=>1item.next#=>2item.next#=>0... 关于ruby-循环遍历数组的元素,我们在StackOverflow上找到一个类似的问题:

  10. ruby - Ruby 中优雅的循环 Elsing - 2

    我必须编写一个Ruby方法:遍历数组,如果其中一个元素符合特定条件则执行Foo。如果没有数组元素符合条件,则执行Bar操作。在任何其他语言中,我会在进入循环之前设置一个bool变量,并在执行Foo时切换它。该变量的值会告诉我是否需要Bar。但这感觉不像Rubyish那样不优雅。谁能提出更好的方法?编辑一些非常好的答案,但由于我本应提及的细节,它们不太有效。Foo所做的事情是对符合条件的数组元素完成的。此外,保证最多有一个元素匹配条件。 最佳答案 是否有任何项目匹配?如果是,则做一些不涉及匹配项目的事情。ifitems.any?{|i

随机推荐