<Data structure> Bidirectional headed circular linked list

content

1. Concept

2. Necessary work

       2.1. Create a doubly linked list structure

       2.2, initialize the linked list

       2.3. Dynamic application node

       2.4, print linked list

       2.5. Destroy the linked list

3. Main functions

       3.1. Insert data before the pos node

                tail plug

                head plug

        3.2. Delete node data at pos

                tail deletion

                header deletion

        3.3. Find data

Fourth, the total code

        List.h file

        List.c file

        Test.c file

5. Expansion: Contrast Sequence List and Linked List


1. Concept

In the previous article, we have learned about the singly linked list, and through the oj topic, we have an in-depth understanding of the linked list with the head node and the linked list with a ring, so as to draw a general review of the picture:

In the linked list we learned, there are actually 8 kinds in total, all of which are any combination of unidirectional and bidirectional, with or without a leader, and with or without a ring.

What we are going to learn today is a two-way-leader-circular linked list. When you hear the name, you feel that the structure is very complicated. It is much more complicated than the one-way-non-leader-non-circular linked list that you have learned before, and it is indeed. First, let's draw a picture to feel the whole:

  • explain:
  1. Bidirectional: It is necessary to ensure that each data stores two pointers next and prev. next points to the next node, prev points to the previous node
  2. Leading: The head node with a sentinel bit is at the head of the data.
  3. Cycle: The next node of the tail node points to the head node of the sentinel bit, and the previous node prev of the sentinel bit points to the tail node, forming a cycle.

The text begins:

2. Necessary work

2.1. Create a doubly linked list structure

Because it is a doubly linked list, there must be two pointers in the structure, one next points to the next node, and one prev points to the previous node.

  • List.h file:
//创建双向链表结构
typedef int LTDataType;   //方便后续更改数据类型,本文以int整型为主
typedef struct ListNode
{
	LTDataType data; //存储数据
	struct ListNode* next; //指向下一个
	struct ListNode* prev; //指向上一个
}LTNode; //方便后续使用,不需要重复些struct

2.2, initialize the linked list

  • Ideas:

The address must be passed during initialization, because the change of the formal parameter will not affect the actual parameter, and the change of pphead will not affect the pList. To pass the address of the pList, use **pphead to receive it. At this time, the assert assertion is required, because two The level pointer address cannot be null. Because it is a doubly circular linked list, both the next and prev of the created sentinel node should point to itself.

  • List.h file: (1)
//初始化链表(二级指针版)
void ListInit(LTNode* pphead);
  • List.c file: (1)
//初始化链表(二级指针版)
void ListInit(LTNode** pphead)
{
	//传二级指针,那么当然要断言
	assert(pphead);
	*pphead = BuyLTNode(0);//因为是带哨兵位的头节点,所以一开始就要给一个节点
	//为了循环,要让哨兵位的next和prev均指向自己
	(*pphead)->next = *pphead; //注意优先级,*pphead要加括号
	(*pphead)->prev = *pphead;
}
  • Notice:

In the previous method, we passed a second-level pointer. Can we pass a first-level pointer? In fact, it is also possible. Just write a function to return the pointer.

  • List.h file: (2)
//初始化链表(一级指针版本)
LTNode* ListInit();
  • List.c file: (2)
//初始化链表(一级指针版)
LTNode* ListInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

2.3. Dynamic application node

  • List.c file:
//创建新节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode; //返回新创建的节点
}

2.4, print linked list

  • Ideas:

Since it is printing, the first thing to understand is that the sentinel bit is not used to store valid data, so there is no need to print, define a cur pointer to iterate, then it should start printing from the next of the phead, and when the cur is finished, go back to stop when phead

  • List.h file:
//打印链表
void ListPrint(LTNode* phead);
  • List.c file:
//打印链表
void ListPrint(LTNode* phead)
{
	assert(phead);//断言
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

2.5. Destroy the linked list

  • Ideas:

Since the linked list is destroyed, it is natural to destroy all elements of the linked list including the sentinel bit, but after all, it cannot be empty when the phead is first passed, so it must be asserted that after all valid data is destroyed, the last Destroy the Sentinel.

Method 1: Traverse

Define a pointer cur, start free from the first valid data of the next phead, save the next one, then free, and traverse in turn

Method 2: Attach the ListErase function

This method is also possible, but every time the Erase is finished, the two nodes before and after will be linked again. Although they will be destroyed in the end, the duck does not need to do this, and it is better to use method 1 directly.

  • List.h file:
//销毁链表
void ListDestory(LTNode* phead);
  • List.c file:
//销毁链表
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	//销毁从第一个节点到尾部的数据
	while (cur != phead)
	{
		LTNode* next = cur->next;
		//ListErase(cur);
		free(cur);
		cur = next;
	}
	//置空哨兵位节点phead
	free(phead);
	phead = NULL;
}
  • Test.c file:
void TestList7()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	//销毁链表
	ListDestory(pList);
	pList = NULL;
}

3. Main functions

3.1. Insert data before the pos node

  • Ideas:

Suppose we have already inserted 4 numbers, and now we want to insert 30 in front of the number 3, then we must first check whether there is a number 3, and if so, insert it. Note: The search function mentioned later needs to be used here, which is directly quoted here, and the detailed explanation can be seen later, the problem is not big!

First, put 30 into the newly created node newnode. In order to achieve bidirectionality, first point the next of the previous data 2 of 3 to the new node newnode, point the prev of newnode to 2, the next of newnode to point 3, and the prev of 3 to point to newnode.

  •  List.h file:
//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x);
  • List.c file:
//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//创建插入数据的新节点
	LTNode* newnode = BuyLTNode(x);
	//链接左侧
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	//链接右侧
	newnode->next = pos;
	pos->prev = newnode;
}
  • Test.c file:
void TestList3()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//寻找数字
	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListInsert(pos, 30); //找到数字3就插入
	}
	ListPrint(pList);//打印
}
  • The effect is as follows:

tail plug

  • Ideas:

First of all, because this linked list is a head node with a sentinel bit, the head node must not be empty, and the assert assertion is required at the beginning. Secondly, tail insertion of a singly linked list needs to find the tail, although a doubly linked list is also required, but it is very simple, and there is no need to traverse the linked list, because the last node of the phead of the sentinel head node points to the tail, which fully reflects the two-way cycle The advantage is that if you find the tail node, you need to create another node to store the inserted data, which is convenient for tail insertion.

  • List.h file:
//尾插
void ListPushBack(LTNode* phead, LTDataType x);

  • List.c file: 1.0
//尾插1.0
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); //断言,防止头节点为空
	LTNode* tail = phead->prev; //找到尾节点,便于后续插入数据
	LTNode* newnode = BuyLTNode(x);//创建新节点
	//将此新插入的尾节点与上一个节点链接起来
	tail->next = newnode;
	newnode->prev = tail;
	//将尾节点与哨兵位phead链接起来构成循环
	newnode->next = phead;
	phead->prev = newnode;
}
  • Test.c file:
void TestList1()
{
	//初始化(法一)
	/*LTNode* pList = NULL;
	ListInit(&pList);*/
	//初始化(法二)
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
}
  • The effect is as follows:

  • Notice:

In the above, we learned to insert data before pos, then imagine that when pos is equal to phead, isn't the front of it (phead) the tail of the linked list, then as it should be, tail insertion can also be done like this:

  • List.c file: 2.0
//尾插2.0
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); 
	ListInsert(phead, x);
}

head plug

  • Ideas:

We have already learned to insert data before pos, so the implementation of head insertion is particularly simple. When pos is the original first data phead->next, the data is inserted before it, then the implementation will soon be the head Plug it in, as follows:

  • List.h file:
//头插
void ListPushFront(LTNode* phead, LTDataType x);
  • List.c file:
//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}
  • Test.c file:
void TestList4()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	for (int i = -2; i <= 0; i++)
	{
		ListPushFront(pList, i); //头插3个数字
	}
	ListPrint(pList);//打印
}
  • The effect is as follows:

3.2. Delete node data at pos

  • Ideas:

Deleting the data at pos is actually very simple, a bit similar to the idea of ​​directly ignoring the pos, or bypassing it. First of all, you need to find the previous node prev and the next node next of pos, link prev and next to each other, and skip pos directly, so that the data of the node at pos can be deleted, remember to release pos to free . Here we take pos as 2 example:

  •  List.h file:
//删除pos处数据
void ListErase(LTNode* pos);
  • List.c file:
//删除pos处数据
void ListErase(LTNode* pos)
{
	assert(pos);
	//定义两个指针保存pos两边的节点
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	//将prev和next链接起来
	prev->next = next;
	next->prev = prev;
	//free释放
	free(pos);
	pos = NULL;
}
  • Test.c file:
void TestList5()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//寻找数字
	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListErase(pos); //删除pos处数据
		pos = NULL; //形参的改变不会影响实参,最好在这置空pos
	}
	ListPrint(pList);//打印
}
  • The effect is as follows:

tail deletion

  • Ideas:

The characteristics of the doubly circular linked list will be reflected again. According to its characteristics, we know that the prev of phead points to the tail node, which is saved with the tail pointer, and then define a pointer tailPrev to point to the prev of the tail. Now we only need to point the next of tailPrev to the sentinel node phead , and then reset the prev of the sentinel bit phead to tailPrev, but don't forget to release the deleted tail node to get free(tail). Remember to assert that the linked list cannot be empty, because the sentinel node cannot be deleted.

  • List.H file:
//尾删
void ListPopBack(LTNode* phead);
  • List.c file: 1.0
//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);//本身就有哨兵位,不能为空,要断言
	assert(phead->next != phead); //防止链表为空,导致删除哨兵位节点
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	//释放尾节点
	free(tail);
	tail = NULL;
	//将链表循环起来
	tailPrev->next = phead;
	phead->prev = tailPrev;
}
  • Test.c file:
void TestList2()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//尾删两次
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);//再次打印
}
  • The effect is as follows:

  •  Notice:

In the previous article, we have learned to delete the data of the node at pos, then when pos is phead->prev, is it the tail node that is deleted, so the tail deletion should be written like this:

  • List.c file: 2.0
//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);//本身就有哨兵位,不能为空,要断言
	assert(phead->next != phead); //防止链表为空,导致删除哨兵位节点
	ListErase(phead->prev);
}

header deletion

  • Ideas:

With the above lessons, we can directly use the previously written function to delete the data at pos to complete. When pos is phead->prev, the position of pos is the tail, and the tail is deleted at this time. Of course, it must be noted that additional assert assertions are required to prevent deleted data from being sentinel nodes.

  • List.h file:
//头删
void ListPopFront(LTNode* phead);
  • List.c file:
//头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); //防止删除哨兵位节点
	ListErase(phead->next);
}
  • Test.c file:
void TestList6()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	//头插3个数字
	ListPushFront(pList, 0);
	ListPushFront(pList, -1);
	ListPushFront(pList, -2);
	ListPrint(pList);//打印
	//尾删3个数字
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);//打印
	//头删3个数字
	ListPopFront(pList);
	ListPopFront(pList);
	ListPopFront(pList);
	ListPrint(pList);//打印
}
  • The effect is as follows:

3.3. Find data

  • Ideas:

Finding data is actually relatively simple. First, define a pointer cur to point to the next of the sentinel bit phead, traverse cur in turn to see if cur->data is the searched data x, if so, return cur, otherwise continue (cur=cur-> next), or NULL if not found.

  • List.h file:
//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
  • List.c file:
//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur; //找到就返回cur
		}
		cur = cur->next;
	}
	return NULL; //找不到就返回空
}

Fourth, the total code

List.h file

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
//创建双向链表结构
typedef int LTDataType;   //方便后续更改数据类型,本文以int整型为主
typedef struct ListNode
{
	LTDataType data; //存储数据
	struct ListNode* next; //指向下一个
	struct ListNode* prev; //指向上一个
}LTNode; //方便后续使用,不需要重复些struct

//初始化链表(二级指针版本)
/*void ListInit(LTNode** pphead);*/
//初始化链表(一级指针版本)
LTNode* ListInit();

//打印链表
void ListPrint(LTNode* phead);
//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//销毁链表
void ListDestory(LTNode* phead);

//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//尾删
void ListPopBack(LTNode* phead);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//头删
void ListPopFront(LTNode* phead);

//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x);
//删除pos处数据
void ListErase(LTNode* pos);

List.c file

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
//创建新节点
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode; //返回新创建的节点
}
//初始化链表(二级指针版)
/*void ListInit(LTNode** pphead)
{
	//传二级指针,那么当然要断言
	assert(pphead);
	*pphead = BuyLTNode(0);//因为是带哨兵位的头节点,所以一开始就要给一个节点
	//为了循环,要让哨兵位的next和prev均指向自己
	(*pphead)->next = *pphead; //注意优先级,*pphead要加括号
	(*pphead)->prev = *pphead;
}*/
//初始化链表(一级指针版)
LTNode* ListInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//打印链表
void ListPrint(LTNode* phead)
{
	assert(phead);//断言
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}
//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur; //找到就返回cur
		}
		cur = cur->next;
	}
	return NULL; //找不到就返回空
}
//销毁链表
void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	//销毁从第一个节点到尾部的数据
	while (cur != phead)
	{
		LTNode* next = cur->next;
		//ListErase(cur);
		free(cur);
		cur = next;
	}
	//置空哨兵位节点phead
	free(phead);
	phead = NULL;
}

//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); //断言,防止头节点为空
	/*
	法一:
	LTNode* tail = phead->prev; //找到尾节点,便于后续插入数据
	LTNode* newnode = BuyLTNode(x);//创建新节点
	//将此新插入的尾节点与上一个节点链接起来
	tail->next = newnode;
	newnode->prev = tail;
	//将尾节点与哨兵位phead链接起来构成循环
	newnode->next = phead;
	phead->prev = newnode;
	*/
	//法二:
	ListInsert(phead, x);
}
//尾删
void ListPopBack(LTNode* phead)
{
	assert(phead);//本身就有哨兵位,不能为空,要断言
	assert(phead->next != phead); //防止链表为空,导致删除哨兵位节点
	/*
	法一:
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	//释放尾节点
	free(tail);
	tail = NULL;
	//将链表循环起来
	tailPrev->next = phead;
	phead->prev = tailPrev;
	*/
	//法二:
	ListErase(phead->prev);
}

//头插
void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}
//头删
void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); //防止删除哨兵位节点
	ListErase(phead->next);
}

//在pos前插入数据
void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//创建插入数据的新节点
	LTNode* newnode = BuyLTNode(x);
	//链接左侧
	pos->prev->next = newnode;
	newnode->prev = pos->prev;
	//链接右侧
	newnode->next = pos;
	pos->prev = newnode;
}
//删除pos处数据
void ListErase(LTNode* pos)
{
	assert(pos);
	//定义两个指针保存pos两边的节点
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	//将prev和next链接起来
	prev->next = next;
	next->prev = prev;
	//free释放
	free(pos);
	pos = NULL;
}

Test.c file

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"
void TestList1()
{
	//初始化(法一)
	/*LTNode* pList = NULL;
	ListInit(&pList);*/
	//初始化(法二)
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
}

void TestList2()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//尾删两次
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);//再次打印
}

void TestList3()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//寻找数字
	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListInsert(pos, 30); //找到数字3就插入
	}
	ListPrint(pList);//打印
}

void TestList4()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	for (int i = -2; i <= 0; i++)
	{
		ListPushFront(pList, i); //头插3个数字
	}
	ListPrint(pList);//打印
}

void TestList5()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数据
	}
	ListPrint(pList);//打印尾插的7个
	//寻找数字
	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListErase(pos); //删除pos处数据
		pos = NULL; //形参的改变不会影响实参,最好在这置空pos
	}
	ListPrint(pList);//打印
}

void TestList6()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	//头插3个数字
	ListPushFront(pList, 0);
	ListPushFront(pList, -1);
	ListPushFront(pList, -2);
	ListPrint(pList);//打印
	//尾删3个数字
	ListPopBack(pList);
	ListPopBack(pList);
	ListPopBack(pList);
	ListPrint(pList);//打印
	//头删3个数字
	ListPopFront(pList);
	ListPopFront(pList);
	ListPopFront(pList);
	ListPrint(pList);//打印
	//销毁链表
	ListDestory(pList);
	pList = NULL;
}

void TestList7()
{
	LTNode* pList = ListInit();
	for (int i = 1; i <= 7; i++)
	{
		ListPushBack(pList, i); //尾插7个数字
	}
	ListPrint(pList);//打印
	//销毁链表
	ListDestory(pList);
	pList = NULL;
}
int main()
{
	//TestList1();
	//TestList2();
	//TestList3();
	//TestList4();
	//TestList5();
	//TestList6();
	TestList7();
	return 0;
}

5. Expansion: Contrast Sequence List and Linked List

difference sequence table linked list
storage space Physically must be continuous Logically contiguous, but not necessarily physically contiguous
random access Support O(1) O(N) is not supported
Insert or delete elements anywhere May need to move elements, inefficient O(N) Just modify the pointer to point to
insert Dynamic sequence table, need to expand when space is not enough no concept of capacity
Application scenarios Elements are efficiently stored + frequently accessed Insert and delete data anywhere
cache utilization high Low
  • Comparison of advantages and disadvantages:
sequence table linked list
advantage

1. The physical space is continuous, which is convenient for random access with subscripts.

2. The CPU cache hit rate will be higher. (Replenish)

1. Apply for free space on demand.

2. Data can be inserted and deleted in O(1) at any position.

shortcoming

1. Just because the physical space is continuous, the space is not enough to need expansion, and the expansion itself must be consumed. Secondly, there is still a certain amount of space waste in the expansion mechanism.

2. Insert and delete in the head or middle, move data, low efficiency, O(N).

1. Subscript random access is not supported.

2. Some algorithms are not suitable for it, such as binary search, sorting, etc.

Guess you like

Origin blog.csdn.net/bit_zyx/article/details/123710485