引言

在链表中,我们可以分成三张情况

  1. 单向或双向
  2. 带头或不带### H3头
  3. 循环或非循环 将他们排列组合,我们可以得到八种情况 图片.png 虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构:

无头单向肺循环链表(无头单链表)

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。 图片.png

带头双向循环链表

带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会++带来很多优势++,实现反而简单了,后面我们代码实现了就知道了。 图片.png

1.初始化链表

定义一个结构体

typedef struct ListNode
{
	LTDateType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

声明初始化结构体

void ListInit(LTNode* phead);

在test.c中测试

void TestList1()
{
	LTNode* plist = NULL;
	ListInit(plist);
}

当我们把plist赋值为NULL,实参改变,但是我们写的ListInit函数中,形参的改变不会影响实参的改变,这里如果想改变实参,就需要传二级指针。如果不传二级指针,我们可以加返回值。

LTNode* ListInit();
LTNode* ListInit()
{
	//哨兵位头结点
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

图片.png test.c中

LTNode* plist = ListInit();//将实参初始化,这时plist就可以接收

这里再强调一下,单链表中的二级指针是因为形参的改变不会影响实参, 因为他没有哨兵位的头节点,我们在test.c中将头节点初始化为NULL,整个链表为空,需要将数据插入到头节点,改变头结点的值,一级指针无法将数据返回给实参,实参得不到改变,所以我们要传二级指针,取到形参的地址,它里面的值改变,才会传到test.c中。其实单链表也可以用 返回值来接收,但是他在test.c中测试的时候,会变成

	SLTNode* plist = NULL;
	plist = SListPusthFront(plist, 1);
	plist = SListPusthFront(plist, 2);
	plist = SListPusthFront(plist, 3);

每次都要用plist接收一下。 而带哨兵位的双向循环链表,我们将值传进去,改变了一次哨兵位头节点的值,将地址传进去,后面的值将其链接即可。带哨兵位的双向循环链表最好用带返回值的来接收。

尾插

尾插的思路跟单链表一样,找到为节点并插入,但是这次我们不需要遍历整个链表找到为节点,因为头结点的前一个就是尾节点,直接就能找到尾节点。

List.h声明

void ListPushBack(LTNode* phead, LTDateType x);
void ListPushBack(LTNode* phead, LTDateType x)
{
	//phead因为指向malloc出来的哨兵位,一定不为空,所以我们断言一下
	assert(phead);

	//定义尾节点来尾插
	//给新节点开辟新的空间
	LTNode* tail = phead->prev;
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;

	//尾插新节点
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

双向循环链表不需要考虑头结点是否为空,因为我们malloc出来的哨兵位,在初始化的时候,头指向phead,尾也指向phead,都不为空,存在prev和next.在单链表中如果为空,tail为空,它不存在next,还要考虑为空的情况。 这就是双向循环链表的好处,看起来复杂,用起来简单。 有的人说为什么双向循环链表这么方便还要有单链表的存在。单链表很大意义上是用来考察能力的,在面试的时候也很喜欢考察这类。

打印链表、

声明

void ListPrint(LTNode* phead);

打印的时候我们是从哨兵位的后面开始打印的,结束循环的条件是当cur走到哨兵位头节点phead的时候结束循环,所以循环条件是,cur!=phead 时进入循环。

void ListPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur!=phead)
	{
		printf("%d ", cur->data);//打印节点的data数据
		cur = cur->next;//让cur继续向下走
	}
	printf("\n");
}

尾删

定义指针tail找到尾,并记录tail的前一个成为新的尾,我们可以定义指针tailPrev来记录tail的前一个,方便找到。记录好后释放尾节点,头节点的prev指向新尾节点,新尾节点的next指向哨兵位头。

void ListPopBack(LTNode* phead)
{
	assert(phead);//判断链表存不存在
	assert(phead->next != phead);//链表为空就不能删

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	phead->prev = tailPrev;
	tailPrev->next = phead;
	free(tail);
}

测试尾插和尾删 test.c

void TestList1()
{
	LTNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);
	ListPrint(plist);

	ListPopBack(plist);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPrint(plist);
}

int main()
{
	TestList1();
	return 0;
}

图片.png

头插

malloc开辟一个新节点newnode,定义一个指针next记录phead的next,因为我们要头插,所以要在哨兵位phead和pehad的下一个next中插入数据。

oid ListPushFront(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* next = phead->next;

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

头删

与尾删思路大同小异。定义一个节点cur记录要删除的节点,定义一个节点记录cur的next方便哨兵位链接。这样可以不用管顺序,直接free要删除的节点,再链接。非常奈斯

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next);

	LTNode* cur = phead->next;
	LTNode* curNext = cur->next;

	phead->next = curNext;
	curNext->prev = phead;
	free(cur);
}

测试一下头插和头删 test.c

void TestList2()
{
	LTNode* plist = ListInit();
	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPrint(plist);

	ListPopFront(plist);
	ListPopFront(plist);
	ListPopFront(plist);
	ListPrint(plist);
}
int main()
{
	//TestList1();
	TestList2();

	return 0;
}

图片.png

找到所在位置

ListFind的逻辑跟ListPrint非常相似,定义一个指针遍历链表,如果没有找到所在数字,继续向下遍历,如果找到,返回指针。

LTNode* ListFind(LTNode* phead, LTDateType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data != x)
		{
			cur = cur->next;
		}
		else
		{
			return cur;
		}
	}
	printf("\n");
}

在任意位置插入,在pos位置前插入

声明

void ListInsert(LTNode* pos, LTDateType x);

这里有的人会定义的是下标位置,都是可以的,这里我们定义pos的节点指针,这样可以通过指针链接。思路跟头插一样,因为在pos之前插入,所以定义一个指针存放pos的前一个位置,再将其连接起来

void ListInsert(LTNode* pos, LTDateType x)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyListNode(x);

	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

写完插入,他有很多好处,比如前面写的头插和尾插我们可以复用,头插的时候pos就是phead->next的位置,尾插时,因为我们是在pos之前插入,所以pos的位置就是phead 图片.png 这样尾插就变成

void ListPushBack(LTNode* phead, LTDateType x)
{
	assert(phead);
	ListInsert(phead, x);//将phead给到pos的位置
}

头插就变成

void ListPushFront(LTNode* phead, LTDateType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

删除pos位置

同样定义两个指针,记录pos的前一个位置和后一个位置,删除pos时,将前后链接起来

void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* curNext=pos->next;
	LTNode* curPrev = pos->prev;

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

测试一下 test.c

void TestList3()
{
	LTNode* plist = ListInit();
	ListPushBack(plist, 1);
	ListPushBack(plist, 2);
	ListPushBack(plist, 3);
	ListPushBack(plist, 4);

	ListPushFront(plist, 1);
	ListPushFront(plist, 2);
	ListPushFront(plist, 3);
	ListPushFront(plist, 4);
	ListPrint(plist);

	LTNode* pos = ListFind(plist, 2);
	if (pos)
	{
		ListErase(pos);
	}
	ListPrint(plist);
}
int main()
{
	TestList3();

	return 0;
}

图片.png 本次代码的链接 Thanks for watching :)