DS linear list linked list

Preface

We introduced the sequence table in the last issue. Its bottom layer is an array. We have also implemented the dynamic version and the static version of the sequence table respectively! And the advantages and disadvantages of the sequence table are analyzed. The advantages are: tail insertion and tail deletion are very efficient, and its time complexity is O(1); the disadvantage is: the efficiency is low when inserting and deleting at the head, and its time complexity is O(N); and even the expansion of the dynamic version will waste space (here is specially introduced when realloc is introduced about dynamic memory management)! ! This was also introduced at the end of the last issue! We wonder if there is a data structure that can be added one by one so that the minimum space is not wasted? The answer is: yes~! He is the linked list we introduce in this issue!

Introduction to the contents of this issue

What is a linked list

Classification of linked lists

Implementation of singly linked list (without header)

Implementation of singly linked list (taking the lead)

Implementation of leading two-way circular linked list

The difference between linked list and sequence list

Table of contents

Preface

Introduction to the contents of this issue

1. What is a linked list?

2. Classification of linked lists

3. Implementation of singly linked list (without leader)

Linked list node type declaration

Dynamically apply for a node

Printing of linked lists

Destruction of linked list

Single linked list tail insertion

Delete the end of a singly linked list

Single linked table header plug

Single linked list header deletion

singly linked list search

insert x before pos

insert x after pos

delete pos position

Delete the next one at pos position

All source code:

4. Implementation of singly linked list (taking the lead)

Linked list node type declaration

Linked list initialization

Destruction of linked list

Printing of linked lists

Dynamically open a node

Single linked list tail insertion

Tail deletion of singly linked list

Single linked table header plug

Header deletion of singly linked list

singly linked list search

Single linked list modification

insert before pos

Insert after pos

delete pos position

Delete the last one after pos

length of linked list

Summarize:

All source code:

5. Take the lead in the implementation of two-way circular linked list

node type declaration

initialization

destroy

Print

Dynamically open new nodes

tail plug

tail delete

Head plug

Header deletion 

Find

Revise

insert before pos

Insert after pos

Delete pos

Delete the next one of pos

Get the length

6. The difference between linked list and sequence list

1. What is a linked list?

A linked list is aphysical storage structurenoncontinuous (non-sequential storage structure), a logically continuous structure for storing data! Its logical continuity is realized by the pointers inside it~!

We introduced something called the self-reference of a structure in the structure issue, which is essentially a declaration of a linked list! OK, let’s take a look back:

struct Node
{
	int data;
	struct Node* next;
};

This is the declaration of a linked list, which is the self-reference of the structure~! He generally calls data here the data field, and next is called the pointer field! The pointer field of the linked list points to the next node of the same type~! What does that mean? Let’s draw another picture to understand:

Does this diagram clearly reflect all the above points? The physical structure is not continuous (because the malloc node is on the heap, and its memory may be continuous when the operating system randomly allocates it, but there is a high probability that it is not Continuous), the addresses of each of their nodes are not continuous, and the logical structure is continuous because the next of the current node stores the address of the next node, so that as long as the address of the first node is used, all nodes in the linked list can be accessed~!

This is like a train, one section after another:

2. Classification of linked lists

There are 8 types of linked list structures, whether it is the leading node, single or bidirectional, whether it is circular or not, the combination result!

Here we first introduce the singly linked list without the leader, then introduce the singly linked list with the leader, and finally implement the most excellent structure in the linked list--"the headed two-way circular linked list! If you are proficient in these three, the others will all be little Karami~! OK, let’s draw it and get to know it:

Not taking the lead:

take the lead:

This is a diagram of the 8 structures of a linked list! In fact, there are two common or commonly used types: headless single linked list and doubly circular linked list! But a singly linked list will not do storage work alone. It will cooperate with others to implement higher-order data structures, such as hash buckets and adjacency lists of graphs, etc.;

The two-way circular linked list will store data separately. For example, do you have any visitors to the TikTok you use every day? In fact, the bottom layer of it is a two-way circular linked list. Each time one comes, one can be inserted. If a person visits your homepage multiple times, it is just a matter of changing the pointer~!

3. Implementation of singly linked list (without leader)

OK, we still use multi-file programming as in the past. The declarations are placed in the .h file, the implementation is placed in the .c file, and the tests are placed in test.c.

At the beginning we have to declare the type of each node of the linked list!

Linked list node type declaration

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SListNode;

Dynamically apply for a node

Since there is no leader here, there is no need to initialize it. The linked list is composed of one or more nodes. We only need to handle its next when applying and it will be OK! We set next to NULL after the initial application is successful, so that even the last one does not need to be processed as NULL separately!

//动态申请一个节点
SListNode* BuySListNode(SLDataType x)
{
	SListNode* newnode = (SListNode*)malloc (sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

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

	return newnode;
}

Printing of linked lists

//打印链表
void SListPrint(SListNode* head)
{
	SListNode* cur = head;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

Destruction of linked list

In order to make the program more robust and prevent memory leaks in the program, we must remember to destroy the linked list after operating it~!

Here we start from the beginning and free each node, but since there is no leader here, we have to use a secondary pointer! ! ! Here head is essentially a formal parameter. Changing the formal parameters does not change the actual parameters. To change the formal parameters and affect the actual parameters, we have to pass the address of the actual parameters!

Finally, remember to leave your head blank after the free is over! Otherwise it is a wild pointer!

//单链表的销毁
void SListDestory(SListNode** head)
{
	assert(head);

	SListNode* cur = *head;
	while (cur)
	{
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*head = NULL;
}

Single linked list tail insertion

Here we first start with a tough task of inserting the tail of a singly linked list without a head node! You still have to use a secondary pointer here, because if the linked list is empty at the beginning, the first thing inserted must be the head, and the head must point to it, so you need to use a secondary pointer to change the pointer. Here head is a secondary pointer. Even if the outer linked list is empty, it cannot be empty, so make an assertion! Then open a new node and determine whether it is the first insertion. If so, just let the head point to him, otherwise just find the tail node and insert it~!

//尾插
void SListPushBack(SListNode** head, SLDataType x)
{
	assert(head);
	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		*head = node;
	}
	else
	{
		SListNode* cur = *head;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = node;
	}

}

Delete the end of a singly linked list

The first thing that comes to mind when deleting this is to determine whether the current linked list is empty! If it is empty, don't delete it. The judgment method is still the same as before, "gentle type" and "violent type". The editor prefers the violent type~! Then we must also consider the situation after deletion, that is, the processing of head. If the last node is deleted, set head to empty, and head here is a formal parameter. To change it, you must use its pointer, that is It’s level two~!

//尾删
void SListPopBack(SListNode** head)
{
	assert(head);
	assert(*head);//没有节点可删

	if ((*head)->next == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		SListNode* cur = *head;
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

Of course, there is another way to write the conventional deletion here, that is, the fast and slow pointer method: use a prv predecessor pointer to record the previous node. When cur->next is not equal to NULL, assign cur to prv. When cur->next is empty, free cur. Just leave prv->next empty!

//尾删
void SListPopBack(SListNode** head)
{
	assert(head);
	assert(*head);//没有节点可删

	if ((*head)->next == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		/*SListNode* cur = *head;
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;*/

		SListNode* cur = *head;
		SListNode* prv = NULL;
		while (cur->next)
		{
			prv = cur;
			cur = cur->next;
		}
		free(cur);
		prv->next = NULL;
	}
}

OK, let’s develop a good habit and write some tests:

void testBack()
{
	SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉
	SListPushBack(&s, 1);
	SListPushBack(&s, 2);
	SListPushBack(&s, 3);
	SListPushBack(&s, 4);
	SListPushBack(&s, 5);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);
	SListPopBack(&s);
	SListPrint(s);
	SListPopBack(&s);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);
	//SListPopBack(&s);//测试尾删是否有Bug
	//SListPrint(s);

	SListDestory(&s);
}

OK, there is no problem with tail insertion and tail deletion! Let's come down and delete the head plug!

Single linked table header plug

Head insertion is relatively simple. The newly opened node next connects to the head and makes the head point to the new node. But you should always pay attention to the situation where the linked list is empty at the beginning, so you have to use a secondary pointer~!

//头插
void SListPushFront(SListNode** head, SLDataType x)
{
	assert(head);
	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		*head = node;
	}
	else
	{
		node->next = *head;
		*head = node;
	}
}

Single linked list header deletion

When deleting here, you must first pay attention to the case of being empty, and secondly, if there is only one node, delete this node and make the head empty. The value of the head must also be changed here, so a secondary pointer must be used. Normal deletion means saving the next node of the current node. Free current node assigns the value of the just saved next node to the current node~!

//头删
void SListPopFront(SListNode** head)
{
	assert(head);
	assert(*head);//没有节点可删

	if ((*head)->next == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		SListNode* tail = (*head)->next;
		free(*head);
		*head = tail;
	}
}

OK, let’s test it:

void testFront()
{
	SListNode* s = NULL;
	SListPushFront(&s, 1);
	SListPushFront(&s, 2);
	SListPushFront(&s, 3);
	SListPushFront(&s, 4);
	SListPushFront(&s, 5);
	SListPrint(s);

	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	//SListPopFront(&s);//测试头删是是否有Bug
	//SListPrint(s);

	SListDestory(&s);
}

OK, no problem, let's implement search and insert before pos and insert after pos!

singly linked list search

It's very simple here, find the address to return the node, otherwise return NULL! This is just searching without changing the content, so the first-level pointer is enough!

//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x)
{
	SListNode* cur = head;
	while (cur)
	{
		if (x == cur->data)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

insert x before pos

Here, an x ​​is inserted before the pos position. There are two ways to write it. You also have to pay attention to the first insertion here! So you have to use secondary pointers! The first is to directly implement it manually without any external force, and the second is to call the head plug to solve the first insertion situation~! And please note that in the first type, you must add break after inserting to exit, otherwise it will be an infinite loop. In the second type of change, you don’t have to worry about this situation~!

//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{
	assert(head);
	assert(pos);

	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		node->next = *head;
		*head = node;
	}
	else
	{
		SListNode* cur = *head;
		while (cur)
		{
			if (cur->next == pos)
			{
				node->next = pos;
				cur->next = node;
				break;
			}
			cur = cur->next;
		}
	}

}
//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{
	assert(head);
	assert(pos);

	if (*head == NULL)
	{
		SListPushFront(head, x);
	}
	else
	{
		SListNode* cur = *head;
		while (cur->next != pos)
		{
			cur = cur->next;
		}

		SListNode* node = BuySListNode(x);
		node->next = pos;
		cur->next = node;
	}

}

insert x after pos

This interface is even simpler. You only need to connect pos->next to node->next and then connect pos->next to node~! Don’t worry about pos->next being empty!

//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);

	SListNode* node = BuySListNode(x);
	node->next = pos->next;
	pos->next = node;
}

OK, let’s test it:

testInsert()
{
	SListNode* s = NULL;
	SListPushFront(&s, 1);
	SListPushFront(&s, 2);
	SListPushFront(&s, 3);
	SListPushFront(&s, 4);
	SListPushFront(&s, 5);
	SListPrint(s);

	SListInsert(&s, SListFind(s, 1), 10);
	SListPrint(s);
	SListInsert(&s, SListFind(s, 1), 100);
	SListPrint(s);

	SListInsertAfter(SListFind(s, 4), 40);
	SListPrint(s);
	SListDestory(&s);
}

OK, no problem, let’s implement deleting the pos position and deleting the next node of the pos position~!

delete pos position

This interface means that when the header and pos are the same, you can directly call header deletion without having to make any judgments yourself. Otherwise, just traverse to find pos and delete it~~!

//删除pos位置
void SListErase(SListNode** head, SListNode* pos)
{
	assert(head);
	assert(*head);//空了就不要删了
	assert(pos);

	if (*head == pos)
	{
		SListPopFront(head);
	}
	else
	{
			SListNode* cur = *head;
			while (cur->next != pos)
			{
				cur = cur->next;
			}

			cur->next = pos->next;
			free(pos);
	}

}

Delete the next one at pos position

This must ensure that there is pos and the next of pos, then save the next of pos, the next of pos is connected to pos->next->next, and then delete the next of the saved pos~!

//删除pos后一个位置
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);

	SListNode* Pnext = pos->next;
	pos->next = Pnext->next;
	free(Pnext);
}

OK, test it out:

void testErase()
{
	SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉
	SListPushBack(&s, 1);
	SListPushBack(&s, 2);
	SListPushBack(&s, 3);
	SListPushBack(&s, 4);
	SListPushBack(&s, 5);
	SListPrint(s);

	SListErase(&s, SListFind(s, 1));
	SListPrint(s);
	SListErase(&s, SListFind(s, 2));
	SListPrint(s);

	SListEraseAfter(SListFind(s, 3));
	SListPrint(s);

	SListDestory(&s);
}

OK, no problem~!

All source code:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SListNode;


//动态申请一个节点
SListNode* BuySListNode(SLDataType x);

//打印链表
void SListPrint(SListNode* head);

//单链表的销毁
void SListDestory(SListNode** head);

//尾插
void SListPushBack(SListNode** head, SLDataType x);

//尾删
void SListPopBack(SListNode** head);

//头插
void SListPushFront(SListNode** head, SLDataType x);

//头删
void SListPopFront(SListNode** head);

//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x);

//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x);

//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x);

//删除pos位置
void SListErase(SListNode** head, SListNode* pos);

//删除pos后一个位置
void SListEraseAfter(SListNode* pos);

#include "SListNode.h"

//动态申请一个节点
SListNode* BuySListNode(SLDataType x)
{
	SListNode* newnode = (SListNode*)malloc (sizeof(SListNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

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

	return newnode;
}

//打印链表
void SListPrint(SListNode* head)
{
	SListNode* cur = head;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

//单链表的销毁
void SListDestory(SListNode** head)
{
	assert(head);

	SListNode* cur = *head;
	while (cur)
	{
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*head = NULL;
}

//尾插
void SListPushBack(SListNode** head, SLDataType x)
{
	assert(head);
	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		*head = node;
	}
	else
	{
		SListNode* cur = *head;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = node;
	}

}

//尾删
void SListPopBack(SListNode** head)
{
	assert(head);
	assert(*head);//没有节点可删

	if ((*head)->next == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		/*SListNode* cur = *head;
		while (cur->next->next)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;*/

		SListNode* cur = *head;
		SListNode* prv = NULL;
		while (cur->next)
		{
			prv = cur;
			cur = cur->next;
		}
		free(cur);
		prv->next = NULL;
	}
}


//头插
void SListPushFront(SListNode** head, SLDataType x)
{
	assert(head);
	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		*head = node;
	}
	else
	{
		node->next = *head;
		*head = node;
	}
}

//头删
void SListPopFront(SListNode** head)
{
	assert(head);
	assert(*head);//没有节点可删

	if ((*head)->next == NULL)
	{
		free(*head);
		*head = NULL;
	}
	else
	{
		SListNode* tail = (*head)->next;
		free(*head);
		*head = tail;
	}
}

//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x)
{
	SListNode* cur = head;
	while (cur)
	{
		if (x == cur->data)
			return  cur;

		cur = cur->next;
	}

	return NULL;
}

//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{
	assert(head);
	assert(pos);

	SListNode* node = BuySListNode(x);
	if (*head == NULL)
	{
		node->next = *head;
		*head = node;
	}
	else
	{
		SListNode* cur = *head;
		while (cur)
		{
			if (cur->next == pos)
			{
				node->next = pos;
				cur->next = node;
				break;
			}
			cur = cur->next;
		}
	}

}

//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);

	SListNode* node = BuySListNode(x);
	node->next = pos->next;
	pos->next = node;
}

//删除pos位置
void SListErase(SListNode** head, SListNode* pos)
{
	assert(head);
	assert(*head);//空了就不要删了
	assert(pos);

	if (*head == pos)
	{
		SListPopFront(head);
	}
	else
	{
			SListNode* cur = *head;
			while (cur->next != pos)
			{
				cur = cur->next;
			}

			cur->next = pos->next;
			free(pos);
	}

}

//删除pos后一个位置
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);

	SListNode* Pnext = pos->next;
	pos->next = Pnext->next;
	free(Pnext);
}
#include "SListNode.h"

void testBack()
{
	SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉
	SListPushBack(&s, 1);
	SListPushBack(&s, 2);
	SListPushBack(&s, 3);
	SListPushBack(&s, 4);
	SListPushBack(&s, 5);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);
	SListPopBack(&s);
	SListPrint(s);
	SListPopBack(&s);
	SListPrint(s);

	SListPopBack(&s);
	SListPrint(s);
	//SListPopBack(&s);//测试尾删是否有Bug
	//SListPrint(s);

	SListDestory(&s);
}

void testFront()
{
	SListNode* s = NULL;
	SListPushFront(&s, 1);
	SListPushFront(&s, 2);
	SListPushFront(&s, 3);
	SListPushFront(&s, 4);
	SListPushFront(&s, 5);
	SListPrint(s);

	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	SListPopFront(&s);
	SListPrint(s);
	//SListPopFront(&s);//测试头删是是否有Bug
	//SListPrint(s);

	SListDestory(&s);
}

testInsert()
{
	SListNode* s = NULL;
	SListPushFront(&s, 1);
	SListPushFront(&s, 2);
	SListPushFront(&s, 3);
	SListPushFront(&s, 4);
	SListPushFront(&s, 5);
	SListPrint(s);

	SListInsert(&s, SListFind(s, 1), 10);
	SListPrint(s);
	SListInsert(&s, SListFind(s, 1), 100);
	SListPrint(s);

	SListInsertAfter(SListFind(s, 4), 40);
	SListPrint(s);
	SListDestory(&s);
}

void testErase()
{
	SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉
	SListPushBack(&s, 1);
	SListPushBack(&s, 2);
	SListPushBack(&s, 3);
	SListPushBack(&s, 4);
	SListPushBack(&s, 5);
	SListPrint(s);

	SListErase(&s, SListFind(s, 1));
	SListPrint(s);
	SListErase(&s, SListFind(s, 2));
	SListPrint(s);

	SListEraseAfter(SListFind(s, 3));
	SListPrint(s);

	SListDestory(&s);
}

int main()
{
	//testBack();
	//testFront();
	//testInsert();
	testErase();
	return 0;
}

OK, the headless (head node without sentinel bit) singly linked list is here~! This should be the most complicated of the three we introduced~! If you master this, the rest will be easy~! Let’s take a look at the singly linked list with the head node of the sentinel bit~!

4. Implementation of singly linked list (taking the lead)

Still declare the type of each node first like without a header:

Linked list node type declaration

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLNode;

Linked list initialization

由于前面是不带头的,所以不需要进行初始化,而这里现在是带哨兵位头结点的~!一开始得要处理好头结点~!处理的方式有两种,一种是在初始的时候无返回值需要传一次二级,其他地方都不用,这显得有点突兀,所以我们采用第二种返回值,将处理好的头结点的地址返回外面接收此时就是带头的了。其实还有一种就是再定义一个结构体,里面是专门维护这个链表的指针,传一级指针也是可以的~~!当然这里采用返回值的方式!

//链表的初始化
SLNode* SLInit()
{
	SLNode* head = (SLNode*)malloc(sizeof(SLNode));
	if (head == NULL)
	{
		perror("head malloc failed");
		exit(-1);
	}
	head->next = NULL;

	return head;
}

链表的销毁

这里销毁也是一样有两种方式一种是传二级啥都不管了,内部会处理好但这里人家都是一级你一个二级显得有些突兀,还有一种就是不传二级外面最后记得把头置空即可!我们这里就采用第二种!

//链表的销毁
void SLDestory(SLNode* head)
{
	assert(head);//头结点不可能为空
	SLNode* cur = head;
	while (cur)
	{
		SLNode* tail = cur->next;
		free(cur);
		cur = tail;
	}
}

链表的打印

//链表的打印
void SLPrint(SLNode* head)
{
	assert(head);

	SLNode* cur = head->next;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

动态开辟一个节点

由于后面会有大量的插入,每次插入都要开新节点,所以我们直接把他封装成一个函数,后续调用起来方便,代码简洁~!

//动态开辟一个节点
SLNode* BuyNode(SLDataType x)
{
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	if (node == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	node->data = x;
	node->next = NULL;

	return node;
}

单链表尾插

由于有头结点,可以不因考虑第一个插入的情况,另外这里的head是头结点,即使链表为空他也不可能为空!因此为了代码的健壮性断言一下还是好的~!


//尾插
void SLPushBack(SLNode* head, SLDataType x)
{
	assert(head);
	SLNode* newnode = BuyNode(x);

	SLNode* cur = head;
	while (cur->next)
	{
		cur = cur->next;
	}

	cur->next = newnode;
}

单链表的尾删

尾删也不许担心删完后置头结点为空,这里带了哨兵位的头不需要担心直接删即可,另外这里还是有两种方法上面已经说过,这里采用快慢指针法~!

//尾删
void SLPopBack(SLNode* head)
{
	assert(head);
	assert(head->next);//空链表
	SLNode* cur = head;

	while (cur->next->next)
	{
		cur = cur->next;
	}

	free(cur->next);
	cur->next = NULL;
}

OK,来测试一把:

void testBack()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPrint(s);

	SLPopBack(s);
	SLPrint(s);
	SLPopBack(s);
	SLPrint(s);
	SLPopBack(s);
	SLPrint(s);
	//SLPopBack(s);//检测尾删是否有Bug
	//SLPrint(s);

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

OK,没有问题,这个是不是相较于上面没带哨兵位的头结点的那个简单多了~!哈哈,OK,我们再来实现一下,头插、头删~!

单链表头插

这相比上面的就很简单了,直接插在哨兵位头结点的后面即可!不用考虑第一个插入改变头的情况~!

//头插
void SLPushFront(SLNode* head, SLDataType x)
{
	assert(head);

	SLNode* newnode = BuyNode(x);
	newnode->next = head->next;
	head->next = newnode;
}

单链表的头删

头删也是比以前的简单,只需判空即可,不需要考虑改变头的问题~!

//头删
void SLPopFront(SLNode* head)
{
	assert(head);
	assert(head->next);//无节点可删

	SLNode* del = head->next;
	head->next = del->next;
	free(del);
}

OK,还是来测试一波:

void testFront()
{
	SLNode* s = SLInit();
	SLPushFront(s, 1);
	SLPushFront(s, 2);
	SLPushFront(s, 3);
	SLPushFront(s, 4);
	SLPrint(s);

	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	//SLPopFront(s);//测试头删是否有Bug
	//SLPrint(s);

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

单链表查找

还是和前面一样找到了返回当前节点的地址,否则返回NULL。

//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x)
{
	assert(head);

	SLNode* cur = head->next;
	while (cur)
	{
		if (x == cur->data)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

这里肯定有小伙伴问这个函数不就查找吗,除了配合insert和erase插入删除还能干啥?其实他还能修改信息~!数据结构学的是数据的物理存储结构、逻辑顺序结构和运算~!这里的运算简单点就是增删查改。上面无头的我们已经进行了增删查,没有实现改(其实是我忘了,刚刚想起哈哈),这里我们可以补在这个上,上面无头的也是一样的~!!!

单链表修改

//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y)
{
	assert(head);

	SLNode* ret = SLFind(head, x);
	assert(ret);
	ret->data = y;
}

在pos前插入

逻辑和上面的一样,这里也不用考虑第一个插入要改变头的情况~!

//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x)
{
	assert(head);
	assert(pos);

	SLNode* newnode = BuyNode(x);
	SLNode* cur = head;
	while (cur->next != pos)
	{
		cur = cur->next;
	}

	newnode->next = pos;
	cur->next = newnode;
}

在pos后插入

//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);

	SLNode* newnode = BuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

OK,则是一波走起:(这里就把修改顺便测了)

void testInsert()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPrint(s);

	SLInsert(s, SLFind(s, 1), 10);
	SLPrint(s);
	SLInsertAfter(SLFind(s, 2), 200);
	SLPrint(s);

	SLModif(s, 3, 300);
	SLPrint(s);

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

OK,没有问题~!再来实现两个erase的接口!

删除pos位置

这里由于是SLFind配合删除的,所以不因担心删完多删的情况!

//删除pos位置
void SLErase(SLNode* head, SLNode* pos)
{
	assert(head);
	assert(pos);

	SLNode* cur = head;
	while (cur->next != pos)
	{
		cur = cur->next;
	}

	cur->next = pos->next;
	free(pos);
}

删除pos后一个

唯一要注意的是pos得有下一个才让删除,否则结束~!

//删除pos位置的后一个
void SLEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
}

链表的长度

这里多加的一个接口是获取链表的长度~!这个不难很容易~!

//获取链表的长度
int SLSize(SLNode* head)
{
	assert(head);

	int size = 0;
	SLNode* cur = head->next;
	while (cur)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

OK,测试一把:

void testErase()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPushBack(s, 4);
	SLPrint(s);

	SLErase(s, SLFind(s, 1));
	SLPrint(s);

	SLEraseAfter(SLFind(s, 2));
	SLPrint(s);

	printf("size = %d\n", SLSize(s));

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

OK,没有问题~!

总结:

带哨兵位的头结点的确比不带头的要简单很多!这两种都很重要以后工作中常用的就是这两种和下面要介绍的带头双向循环链表~!而且他这里插入和删除可以用Insert和Erase简化!

全部源码:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLNode;


//链表的初始化
SLNode* SLInit();

//链表的销毁
void SLDestory(SLNode* head);

//链表的打印
void SLPrint(SLNode* head);

//动态开辟一个节点
SLNode* BuyNode(SLDataType x);

//尾插
void SLPushBack(SLNode* head, SLDataType x);

//尾删
void SLPopBack(SLNode* head);

//头插
void SLPushFront(SLNode* head, SLDataType x);

//头删
void SLPopFront(SLNode* head);

//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x);

//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y);

//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x);

//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x);

//删除pos位置
void SLErase(SLNode* head, SLNode* pos);

//删除pos位置的后一个
void SLEraseAfter(SLNode* pos);

//获取链表的长度
int SLSize(SLNode* head);
#include "SLT.h"

//链表的初始化
SLNode* SLInit()
{
	SLNode* head = (SLNode*)malloc(sizeof(SLNode));
	if (head == NULL)
	{
		perror("head malloc failed");
		exit(-1);
	}
	head->next = NULL;

	return head;
}

//链表的销毁
void SLDestory(SLNode* head)
{
	assert(head);//头结点不可能为空
	SLNode* cur = head;
	while (cur)
	{
		SLNode* tail = cur->next;
		free(cur);
		cur = tail;
	}
}


//链表的打印
void SLPrint(SLNode* head)
{
	assert(head);

	SLNode* cur = head->next;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}


//动态开辟一个节点
SLNode* BuyNode(SLDataType x)
{
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	if (node == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	node->data = x;
	node->next = NULL;

	return node;
}

//尾插
void SLPushBack(SLNode* head, SLDataType x)
{
	assert(head);
	SLNode* newnode = BuyNode(x);

	SLNode* cur = head;
	while (cur->next)
	{
		cur = cur->next;
	}

	cur->next = newnode;
}


//尾删
void SLPopBack(SLNode* head)
{
	assert(head);
	assert(head->next);//空链表
	SLNode* cur = head;

	while (cur->next->next)
	{
		cur = cur->next;
	}

	free(cur->next);
	cur->next = NULL;
}

//头插
void SLPushFront(SLNode* head, SLDataType x)
{
	assert(head);

	SLNode* newnode = BuyNode(x);
	newnode->next = head->next;
	head->next = newnode;
}

//头删
void SLPopFront(SLNode* head)
{
	assert(head);
	assert(head->next);//无节点可删

	SLNode* del = head->next;
	head->next = del->next;
	free(del);
}

//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x)
{
	assert(head);

	SLNode* cur = head->next;
	while (cur)
	{
		if (x == cur->data)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y)
{
	assert(head);

	SLNode* ret = SLFind(head, x);
	assert(ret);
	ret->data = y;
}

//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x)
{
	assert(head);
	assert(pos);

	SLNode* newnode = BuyNode(x);
	SLNode* cur = head;
	while (cur->next != pos)
	{
		cur = cur->next;
	}

	newnode->next = pos;
	cur->next = newnode;
}

//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);

	SLNode* newnode = BuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

//删除pos位置
void SLErase(SLNode* head, SLNode* pos)
{
	assert(head);
	assert(pos);

	SLNode* cur = head;
	while (cur->next != pos)
	{
		cur = cur->next;
	}

	cur->next = pos->next;
	free(pos);
}

//删除pos位置的后一个
void SLEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
}

//获取链表的长度
int SLSize(SLNode* head)
{
	assert(head);

	int size = 0;
	SLNode* cur = head->next;
	while (cur)
	{
		++size;
		cur = cur->next;
	}

	return size;
}
#include "SLT.h"

void testBack()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPrint(s);
	SLPopBack(s);
	SLPrint(s);
	SLPopBack(s);
	SLPrint(s);
	SLPopBack(s);
	SLPrint(s);
	//SLPopBack(s);//检测尾删是否有Bug
	//SLPrint(s);
	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

void testFront()
{
	SLNode* s = SLInit();
	SLPushFront(s, 1);
	SLPushFront(s, 2);
	SLPushFront(s, 3);
	SLPushFront(s, 4);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	SLPopFront(s);
	SLPrint(s);
	//SLPopFront(s);//测试头删是否有Bug
	//SLPrint(s);

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

void testInsert()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPrint(s);

	SLInsert(s, SLFind(s, 1), 10);
	SLPrint(s);
	SLInsertAfter(SLFind(s, 2), 200);
	SLPrint(s);

	SLModif(s, 3, 300);
	SLPrint(s);

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

void testErase()
{
	SLNode* s = SLInit();
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPushBack(s, 4);
	SLPrint(s);

	SLErase(s, SLFind(s, 1));
	SLPrint(s);

	SLEraseAfter(SLFind(s, 2));
	SLPrint(s);

	printf("size = %d\n", SLSize(s));

	SLDestory(s);
	s = NULL;//记得把头置空,避免野指针
}

int main()
{
	//testBack();
	//testFront();
	//testInsert();
	testErase();
	return 0;
}

五、带头双向循环链表的实现

我们上面实现的两种都是单链表,他们头插头删的效率很高,但在中间或尾部插入删除时的效率就很低,为了解决这个问题我们在定义节点类型时在加入一个前驱指针来记录当前节点的前一个节点的地址,这样插入删除就很OK了~!当最后一个时让他的后继指针指向头结点,第一个的前驱指向最后一个节点,这样就实现了循环而且任何节点的插入删除都很高~!OK,下面是图:

OK,下面我们就来实现一下~!

结点类型声明

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prv;
	struct ListNode* next;
}ListNode;

初始化

//初始化
ListNode* ListInit()
{
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (!head)
	{
		perror("malloc failed");
		exit(-1);
	}

	head->prv = head;
	head->next = head;

	return head;
}

销毁

这里与上面单向带有不一样的是!这里因为是循环的最后的next指向哨兵位的头,所以不能和上面一样直接从head开始销毁,应该从head->next开始等所有的销毁完了,在把head置空即可~!这是因为是一级指针所以外面的调用者在最后记得置空head~!

//销毁
void ListDestory(ListNode* head)
{
	assert(head);

	ListNode* cur = head->next;
	while (cur != head)
	{
		ListNode* tail = cur->next;
		free(cur);
		cur = tail;
	}

	free(head);
}

打印

//链表的打印
void ListPrint(ListNode* head)
{
	assert(head);

	ListNode* cur = head->next;
	while (cur != head)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
}

动态开辟新节点

//动态开辟一个节点
ListNode* BuyNode(LTDataType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (!node)
	{
		perror("malloc failed");
		exit(-1);
	}

	node->data = x;
	node->prv = NULL;
	node->next = NULL;

	return node;
}

尾插

这里比单链表好的就是不用遍历找尾直接就可以找到,但注意的是要把prv前驱指针处理好~!如果不熟练可以多定义个指针,如果熟悉了就可以多个->next->prv哈哈~!下面是两种写法!

//尾插
void ListPushBack(ListNode* head, LTDataType x)
{
	assert(head);

	ListNode* newnode = BuyNode(x);
	ListNode* tail = head->prv;
	tail->next = newnode;
	newnode->prv = tail;
	newnode->next = head;
	head->prv = newnode;

	/*head->prv->next = newnode;
	newnode->prv = head->prv;
	newnode->next = head;
	head->prv = newnode;*/
}

尾删

尾删也是一样可以多定义指针也可以直接来!也要注意处理好前驱~!下面是两种方式!

//尾删
void ListPopBack(ListNode* head)
{
	assert(head);
	assert(head->next != head);//链表为空

	ListNode* tail = head->prv;
	ListNode* tailPrv = tail->prv;

	tailPrv->next = head;
	head->prv = tailPrv;
	free(tail);

	/*ListNode* del = head->prv;
	head->prv->prv->next = head;
	head->prv = head->prv->prv;
	free(del);*/
}

OK还是测试一波:

void testBack()
{
	ListNode* L = ListInit();
	ListPushBack(L, 1);
	ListPushBack(L, 2);
	ListPushBack(L, 3);
	ListPrint(L);

	ListPopBack(L);
	ListPrint(L);
	ListPopBack(L);
	ListPrint(L);
	ListPopBack(L);
	ListPrint(L);
	//ListPopBack(L);//测试尾删是否有Bug
	//ListPrint(L);

	ListDestory(L);
	L = NULL;
}

OK,没有问题,再来搞两个头插和头删~!

头插

//头插
void ListPushFront(ListNode* head, LTDataType x)
{
	assert(head);

	ListNode* newnode = BuyNode(x);
	ListNode* tail = head->next;
	newnode->next = tail;
	tail->prv = newnode;
	head->next = newnode;
	newnode->prv = head;

	/*newnode->next = head->next;
	head->next->prv = newnode;
	head->next = newnode;
	newnode->prv = head;*/
}

头删 

//头删
void ListPopFront(ListNode* head)
{
	assert(head);
	assert(head->next != head);

	ListNode* del = head->next;
	head->next = del->next;
	del->next->prv = head;
	free(del);
}

OK,来测试一波:

void testFront()
{
	ListNode* L = ListInit();
	ListPushFront(L, 1);
	ListPushFront(L, 2);
	ListPushFront(L, 3);
	ListPrint(L);

	ListPopFront(L);
	ListPrint(L);
	ListPopFront(L);
	ListPrint(L);
	ListPopFront(L);
	ListPrint(L);
	//ListPopFront(L);//测试头删是否有Bug
	//ListPrint(L);

	ListDestory(L);
	L = NULL;
}

OK,没有bug!我们继续:

查找

这里还是和前面一样找到了返回当前节点的地址否则返回NULL,实际上在很多常见下就是这样的~!

//查找
ListNode* ListFind(ListNode* head, LTDataType x)
{
	assert(head);

	ListNode* cur = head->next;
	while (cur != head)
	{
		if (x == cur->data)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

修改

记得判断找不到的情况,我这里采用的是暴力检查!!!

//修改
void ListModif(ListNode* head, LTDataType x, LTDataType y)
{
	assert(head);

	ListNode* ret = ListFind(head, x);
	assert(ret);
	ret->data = y;
}

在pos前插入

这里注意的是由于查找函数已经把pos查找好了,且有前驱指针所以不需要遍历找pos,直接链接即可~!

//在pos前插入
void ListInsert( ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = BuyNode(x);
	ListNode* Prv = pos->prv;
	newnode->next = pos;
	pos->prv = newnode;
	Prv->next = newnode;
	newnode->prv = Prv;
}

在pos后插入

//在pos后插入
void ListInsertAfter(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newnode = BuyNode(x);
	ListNode* tail = pos->next;
	newnode->next = tail;
	tail->prv = newnode;
	pos->next = newnode;
	newnode->prv = pos;
}

OK,测试一波:

void testInsert()
{
	ListNode* L = ListInit();
	ListPushBack(L, 1);
	ListPushBack(L, 2);
	ListPushBack(L, 3);
	ListPrint(L);

	ListInsert(ListFind(L, 1), 10);
	ListPrint(L);

	ListInsertAfter(ListFind(L, 3), 300);
	ListPrint(L);

	ListDestory(L);
	L = NULL;
}

OK,没有问题~!

删除pos

//删除pos位置
void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* Prv = pos->prv;
	ListNode* tail = pos->next;
	Prv->next = tail;
	tail->prv = Prv;
	free(pos);
}

删除pos的下一个

这里得注意的是又是是循环的。如果pos没有下一个就指向head,所以判断有没有pos的下一个必须有头~!!!

//删除pos位置的下一个
void ListEraseAfter(ListNode* head, ListNode* pos)
{
	assert(pos);
	assert(pos->next != head);//没有下一个节点可删

	ListNode* tail = pos->next;
	pos->next = tail->next;
	tail->prv = pos;
	free(tail);
}

OK,测试一把~!

void testErase()
{
	ListNode* L = ListInit();
	ListPushBack(L, 1);
	ListPushBack(L, 2);
	ListPushBack(L, 3);
	ListPrint(L);

	ListErase(ListFind(L, 1));
	ListPrint(L);

	ListEraseAfter(L, ListFind(L, 2));
	ListPrint(L);

	ListDestory(L);
	L = NULL;
}

没有问题,上面有个修改函数忘了测试了,等在写一个长度一起在这个里面测试了:

获取长度

//获取链表的长度
int ListSize(ListNode* head)
{
	assert(head);

	int size = 0;
	ListNode* cur = head->next;
	while (cur != head)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

OK,测试一下:

void testErase()
{
	ListNode* L = ListInit();
	ListPushBack(L, 1);
	ListPushBack(L, 2);
	ListPushBack(L, 3);
	ListPrint(L);
	printf("size = %d\n", ListSize(L));

	ListErase(ListFind(L, 1));
	ListPrint(L);

	ListEraseAfter(L, ListFind(L, 2));
	ListPrint(L);
	
	printf("size = %d\n", ListSize(L));
	ListModif(L, 2, 200);
	ListPrint(L);

	ListDestory(L);
	L = NULL;
}

OK,这就是带头双向循环链表!链表的实现也就到此结束了,下面我们来对顺序表和链表做一个全面的对比!

六、链表和顺序表的区别

这里我们先来用单链表和顺序表对比,然后再用链表的最优结构---->带头双向循环链表来对比~!

OK,我们再来对比一下顺序表和带头双向循环链表的区别~!

这就是他们的区别,这里可以看出如果你需要大量查找和排序等操作时顺序表是优选,如果说是频繁的插入删除的话链表优选~!不能说他们谁好谁不好,知识应用的场景不同,他们其实是互补的数据结构~!OK,这里提到了一个东西叫缓存命中率~!这个是计算机存成原理里面的概念,这里实际上说的是存储体系和局部性原理~!OK我们大概谈谈:缓存利用率全称:CPU高速缓存利用率

我们一般的计算机中的三大件是:cpu、内存、和本地磁盘~!我们写的数据结构的代码都在内存中。计组中有个存储器层次体系,就是把每个存储器按快慢给分层了,我们来看看:

这里它实际上分为四层(按照价格以及快慢):

寄存器和缓存实际上作用差不多,都是加载数据给cpu处理的,小的数据直接由寄存器来例如一个变量,大的数据由缓存去内存加载到cpu处理,例如数组!为什么CPU不直接到内存中去访问呢?原因很简单,CPU太快了内存和他不是一个等级,那内存以下的就更不要说了。

为了解决这个问题电脑厂商会在CPU周围多弄几个寄存器和多级缓存;他们的速度是跟的上CPU的。这里你可能还会问既然有内存了为什么要磁盘呢?在这里有两方面的原因:一方面最直接的原因贵!另一方面是内存是带电存储而磁盘不是。内存直接能和上级交流磁盘则不行需要先加载到内存在于上级交流!

另外还有一个是局部性原理,他分为局部性时间原理和局部性空间原理:下面是我找的一本计组上的解释:

为什么说顺序表的缓存利用率高呢?根据局部性原理,CPU在访问万当前数据后它的下一个很有可能会被访问到,所以缓存会一次加载不止一个数据而是这个数据和后面的几个,具体几个得看硬件!这样cpu每次访问时缓存已经把顺序表的内容加载过去了,效率会高很多,原因是他们的地址是连续的!链表之所以低得原因是它的每个节点的内存地址大概率是不连续的,每一次访问一个节点的内容都要加载,而且他有可能会加载错造成缓存污染~!

OK,好兄弟,链表就分享到这里!我们下期再见~~!

Guess you like

Origin blog.csdn.net/m0_75256358/article/details/133416026