[Data structure] singly linked list (super complete)

insert image description here


Preface:
 Last time we shared a structural sequence table in the linear table, which has some disadvantages, such as: when inserting or deleting elements in the middle or at the head, the time complexity is O ( N ) O( N )O ( N ) efficiency is relatively low, and the sequence table also consumes time and space when it is expanded. Since we expand by twice every time, it is very likely that the expansion will not be used up. The phenomenon of wasting space. How to solve these problems? Then you need to use another linear structure linked list .

1. What is a linked list?

1.1 Definition

 The linked list is a non-continuous and non-sequential storage structure in the physical storage structure . The logical order of the data elements is realized through the link order of the pointers in the linked list .
 A simple understanding is that each data element in the linked list is stored independently. When a data element needs to be stored, a piece of memory is applied to the memory space to store the current data, and each data element is connected in series through the address. Therefore, for the structure of the linked list, not only the current data element needs to be stored, but also the address of the next element needs to be stored, so that the data elements can be connected together. Usually, we combine the data element to be stored with the address of the next data It is called a node in the linked list . This node consists of two parts of data, so a structure can be defined to create the node.
Graphic:
insert image description here

1.2 Classification of linked lists

 The linked list will be classified and combined according to whether it is the leading node, whether it is a doubly linked list, or whether it is a circular linked list. Therefore, there are a total of eight specific implementation forms of the linked list.
 Among them, the head node is a node of the same type attached before the first node of the linked list. We call this node the head node . Of course, there is no difference in form between the head node and the ordinary node, so the head node is also divided into Data area and pointer area , it is recommended not to store anything in the data area of ​​the head node. In some places, the length of the linked list will be stored in the sequence area of ​​the head node. In fact, this is not appropriate, because the length of the linked list (that is, the length of the node Number) must be an intinteger variable of type. Only when the linked list stores all integer data, the data field of the node is an integer. At this time, it is possible to store the length of the linked list in the data field of the head node, but when When the linked list stores characters or other user-defined types, the data field of the node is not an integer. At this time, it is obviously impossible to store the length of the linked list in the data field of the head node. Therefore, in order to make the linked list we wrote more universal, it is not recommended to store the length of the linked list in the data field of the head node. The pointer field of the head node stores the address of the first node in the linked list. Regarding the advantages of linked lists, here is a suspense. I believe that with the deepening of learning, friends will naturally appreciate the advantages of the head node.
insert image description here

 Here you need to pay attention to the distinction between the head node and the head pointer , they are two different things.

  • The head node is a node, which is essentially a structure variable, distinguishing between data fields and pointer fields
  • The head pointer is a pointer, which is essentially a pointer variable of structure type. It does not distinguish between data fields and pointer fields. It only stores the address of the first node in the linked list.

 A doubly linked list, that is, there are two pointer fields in a node, one stores the address of the node before the current node, and the other stores the address of the node after the current node. As we learn more, we will discover the advantages of the doubly linked list, so I won't go into details here.
insert image description here

 Circular linked list, that is, the pointer field of the last node of the linked list stores the address of the first node, so that the entire linked list forms a ring to achieve a circular effect.
insert image description here

So far, the classification of linked lists has been introduced to everyone. Although linked lists of eight different structures  can be combined according to different classification standards of linked lists , as long as we master the two structures of headless one-way non-circulation and leading two-way loop , the rest The six can easily get started. So below I will introduce these two structures in detail.

2. Headless one-way non-circular linked list

insert image description here

2.1 Structure

typedef int SLTDataType;

typedef struct SListNode
{
    
    
	SLTDataType data;//数据域
	struct SListNode* next;//指针域
}SLTNode

 The structure of the headless one-way non-circular linked list only needs a data field and a pointer field . The type of the data field is the type of data to be stored. In order to make the linked list more universal, typedefthe type renaming is used here. The pointer field is to store the address of the next node , so its type is a pointer of the structure type . It should be noted that the type of the structure struct SListNodemust be written in full, cannot be omitted, and cannot SLTNodebe used to declare the pointer, because in The structure type has not been renamed when the pointer is declared. Struct types are renamed for convenience.

The structural difference between the linked list and the sequence table:
insert image description here
 As can be seen from the figure, each data of the linked list is directly stored in a structure variable, and multiple structure variables together form a linked list, while the sequence table is in a structure On the basis of body variables, its members arrpoint to the space of dynamic application. The data in the sequence table is not directly stored in the structure variable, but is stored in the open space of dynamic application. A sequence table only corresponds to one structure variable. The difference in structure leads to a different meaning pList == NULLfrom ps == NULLthe expression, pList == NULLwhich means that the current linked list is an empty linked list. Of course, the empty linked list is also a linked list, but there is no data in it, and it ps == NULLmeans that this sequence table does not exist at all. Note here: ps == NULLIt does not indicate an empty sequence table, ps->size == 0but an empty sequence table (provided that ps is not empty).

2.2 How to traverse linked list data

 After knowing what a linked list is and the structure of a linked list, we need to use the linked list, that is, to complete the work of adding, deleting, checking, and modifying the linked list . Before doing these tasks, we must first know how to traverse the linked list to access each data in the linked list, and then we can complete the previous work. The traversal process is shown in the following code:

void SLTPrint(SLTNode* phead)
{
    
    
	SLTNode* cur = phead;
	while (cur != NULL)
	{
    
    
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

 First of all, the formal parameter receives the address of the first node in the linked list. To traverse the linked list, of course, we must start from the first node. I believe everyone can easily understand this. The next step is to traverse. Maintained, it can be said that all operations on the linked list are maintained with a pointer, and the purpose of accessing different nodes is achieved by changing the pointer, because the linked list is composed of multiple nodes, to put it bluntly, it is multiple Structural variables are composed together. If it is passed by value, we have to create as many nodes in the function as there are nodes in the linked list if it is passed by value. This not only wastes space, but also greatly increases the complexity of our problem-solving Spend. After clarifying the use of pointers to traverse the linked list, the next step is to move the pointer. cur = cur->next;This statement enables the pointer to move, and assigns curthe address stored in the pointer field of the pointed node to curitself, and curthe pointer field of the pointed node stores the address of the next node. Until , cur == NULLit means that the entire linked list has been traversed. It should be noted that the condition for the end of the traversal cannot be: cur->next != NULL, because cur->next == NULLthe description curpoints to the last node, and the traversal does not end. A physical schematic diagram and a logical schematic diagram of the traversal process are given below.
Schematic diagram of physical structure:
insert image description here
Schematic diagram of logical structure:
insert image description here

2.3 Rear plug

 The first step of tail insertion is of course to create a node (that is, a variable of structure type) to store data. Note that tail insertion is implemented through functions, so it cannot SLTNode newnodebe used to The node created in this way is a local variable, and the local variable is destroyed when the function ends, usually by mallocapplying for a space on the heap to store data. After the node is created, it is necessary to find a way to link this node to the end of the original linked list, so the next step is to find the last node of the original linked list, and then let the pointer field of the last node store the address of the newly created node, thus realizing After linking up, let’s demonstrate the process of tail insertion through the schematic diagram of logical structure and schematic diagram of physical structure:
Logical structure diagram:
insert image description here
Physical structure diagram:
insert image description here
 Tail insertion There is another situation that needs attention, that is, when the linked list is empty. As mentioned above, a linked list is empty, which means that the linked list has only one node, and the address of the node is 0x00000000. The first step is to create a node to store the data to be inserted first, and then? Link this node to 0x00000000the node with this address? The answer is no, because 0x00000000the address space where we are not allowed to access at will, it belongs to the area strictly controlled by the operating system, which means that we cannot access the pointer field of this space and store the address of the new node in it.
 The correct way to insert an empty linked list is to directly use the newly created node as the head node . This means: the address stored in the head pointer needs to be modified to the address of the newly created node! , remember that all operations of tail insertion are implemented by encapsulating them into functions. For functions, the change of formal parameters will not affect the actual parameters , but here you need to modify the address stored in a pointer variable, and hope that after modification in the function , it still takes effect outside the function, so here we need to pass the address , that is, pass the address of the header pointer, the formal parameter of the address of the address naturally needs to be received by a secondary pointer , which is denoted here as pphead. Note: This secondary pointer cannot be empty, because it stores the address of the pointer to the head node. If it is empty, it means that there is no pointer to the head node, which means that the linked list does not exist. It is necessary to distinguish between the absence of the linked list and the How are empty linked lists represented?

  • Empty linked list : The head pointer is empty, which pList == NULLmeans it is an empty linked list. Some operations allow an empty linked list, such as traversal, tail insertion, and head insertion data, while some cases do not allow an empty linked list. For example, delete the head and delete the data at the end. Therefore, we need to check according to the specific situation.
  • The linked list does not exist : pphead ==NULLit means that the linked list does not exist. Of course, the operation of the linked list is not allowed if the linked list does not exist, so as long as it is used, ppheadit must be checked.
    Code:
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
    
    
	assert(pphead);//pphead不能为空,pphead为空说明里面没有存指向头节点指针的地址,那就说明没有链表
	SLTNode* newnode = BuySLTNode(x);//由于在头插和随机插入的过程中也会涉及到节点的创建,所以这里把节点的创建单独封装了一个函数

	if (*pphead == NULL)
	{
    
    
		*pphead = newnode;
	}
	else
	{
    
    
		SLTNode* cur = *pphead;
		while (cur->next != NULL)//假如链表为空这里就非法访问了,因此要先判断
		{
    
    
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

2.4 Create a new node

SLTNode* BuySLTNode(SLTDataType x)
{
    
    
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
    
    
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2.5 plug

 Head plugging is obviously to change the address where the head pointer is stored, so the formal parameter also needs to pass a secondary pointer. Head insertion does not need to consider the case of an empty linked list
. Code implementation:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    
    
	assert(pphead);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

2.6 Tail deletion

 To delete the tail, you must first traverse the linked list to find the last node and release it, and find the penultimate node to change the address stored in its pointer field NULL. So define two pointers to let them traverse the linked list at the same time, one to find the tail, and the other to find the penultimate node. It should be noted that in the case of an empty linked list and a linked list with only one node, the empty linked list cannot be tail deleted, and the linked list with only one node will become an empty linked list after tail deletion, which means that the address stored in the head pointer needs to be changed , so the tail-deleted formal parameter also needs to pass the secondary pointer.
Code:

void SLTPopBack(SLTNode** pphead)
{
    
    
	assert(pphead);
	assert(*pphead);//暴力检查是否为空链表,空链表不能删数据
	//检查链表是否为空
	/*if (*pphead == NULL)//温柔的进行检查
	{
		return;
	}*/
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
    
    
		free(*pphead);
		*pphead = NULL;
	}
	//有多个节点
	else
	{
    
    
		//找尾
		SLTNode* prev = *pphead;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
    
    
			prev = tail;
			tail = tail->next;
		}

		free(tail);
		tail = NULL;

		prev->next = NULL;//假如只有一个节点这里就会非法访问
	}	
}

2.7 Header deletion

 Head deletion obviously needs to change the address stored in the head pointer, so the formal parameter still needs to pass the secondary pointer. Head deletion only needs to pay attention to whether the linked list is empty, and the empty linked list cannot be deleted. In addition, remember to release the original head node when deleting the head node, so you need to keep the address of the original head node before changing the head node, otherwise you will not be able to find the original head node after replacing the new head node .
Code display:

void SLTPopFront(SLTNode** pphead)
{
    
    
	if (*pphead == NULL)//这里也可以直接用assert来断言
	{
    
    
		return;
	}
	SLTNode* tail = *pphead;
	*pphead = (*pphead)->next;//假如链表为空,这里就会发生越界,因此要判断链表是否为空
	free(tail);
	tail = NULL;
}

2.8 Singly linked list lookup

 In fact, it is to traverse the linked list, but only return the address that appears for the first time. Search can be used as modification. After we find the address of the node, we can use the address to modify the data stored in the data field.

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
    
    
	SLTNode* ptr = phead;
	while (ptr != NULL)
	{
    
    
		if (ptr->data == x)
		{
    
    
			return ptr;
		}
		else
		{
    
    
			ptr = ptr->next;
		}
	}
	return NULL;
}

2.9 insert before pos position

 It is similar to tail insertion, but at this time, you only need to traverse the linked list to find the previous node at pos position. You also need to pay attention to the situation that pos is the head node. At this time, it becomes head insertion, and you need to change the address stored in the head pointer. Therefore, the formal parameters of the function need to pass the secondary pointer.

//在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
    
    
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
    
    
		SLTPushFront(pphead, x);
	}
	else
	{
    
    
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
    
    
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

 The above method of inserting in front of the pos position needs to know the address of the head node

2.10 Delete pos location data

 pos may be the address of the head node, so the formal parameter needs to use a secondary pointer.

//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
    
    
	assert(pphead);
	assert(*pphead);//空链表不能删
	assert(pos);
	if (pos == *pphead)
	{
    
    

		SLTPopFront(pphead);
	}
	else
	{
    
    
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
    
    
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;//其实没用,形参的改变不改变实参
	}
}

2.11 Insert after the pos position

 Here you need to pay special attention to the order of address assignment. There are two correct assignment sequences for your reference:

  • First let newnodethe pointer field store posthe address of the next node, and then let posthe pointer field store newnodethe address
  • Use the intermediate variable posto save the address of the following node first, then let posthe pointer field store newnodethe address, and finally let newnodethe pointer field store the address saved in the intermediate variable in the first step.
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
    
    
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	SLTNode* tmp = pos->next;
	pos->next = newnode;
	newnode->next = tmp;
}

2.12 Delete the data behind the pos position

 Note that it cannot be written as follows: pos->next = pos->next->next. Although this writes out the node behind the pos position from the linked list, it does not actually delete it, because each node is applied for on the heap, and it mallocmust be actively released when it is not in use. That is to say free, return this space to the operating system, otherwise it will cause a memory leak. However, if the above statement is written, the node behind pos will be lost and cannot be released. The correct way is to save the address of the node behind pos before executing this statement.

void SLTEraseAfter(SLTNode* pos)
//只能删除pos位置后面的节点,不能删除pos节点
//因为pos节点的前一个节点无从得知
{
    
    
	assert(pos);
	assert(pos->next);
	SLTNode* tmp = pos->next->next;//这里先保存了pos后面的后面的节点的地址,也是可以的
	free(pos->next);
	pos->next = tmp;
}

 Today's sharing is over here! If you think the article is not bad, you can support it three times in a row . Your support is the driving force for Chunren to move forward!
insert image description here

Guess you like

Origin blog.csdn.net/weixin_63115236/article/details/131426953