[Data Structure Elementary] Singly Linked List

As the name suggests, a singly linked list is a chain of adjacent nodes connected by a one-way pointer to form a chain structure , so how to connect these nodes for easy management?

Table of contents

Singly linked list definition

apply for space

create node

print linked list

tail plug

 tail delete

 plug

head delete

look up

 insert

 delete

delete after pos

pos position delete

 freed

full code


Singly linked list definition

typedef int SLDateType
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

 It should be noted that the typedef defined when the structure is defined needs to take effect after the structure is out , and cannot be used directly in the structure.

When defining a singly linked list node, we can only apply for space on the heap, because the local declaration cycle disappears after leaving the scope, while the global is in the static area, and it is impossible to dynamically add, delete, check and modify data, so in The heap area makes management more flexible and efficient .

apply for space

Inserting data in the sequence table requires checking whether the node space is sufficient to choose whether to expand. In the singly linked list, each node needs to be dynamically opened . We can implement a function to meet our needs.

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//节点
	if(newnode == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = nullptr;//置空,否则野指针
	return newnode;
}

Because each applied node here is independent , it can be received by a temporary object, unlike the sequence table, where development failures can be received. 

Let's test it first:

c6f89d73830d4478a10938e9c475345c.png It can be seen that we successfully applied for the space, but now they are independent of each other, so we need to link them. Before that, introduce a very important concept to everyone: logical structure and physical structure

 1672796063a647c9aa895584595b88ea.png

Here n1, n2... point to the space on the heap, and itself (address) is on the stack.

    n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = NULL;

0dcc758cdfe04bedac7cff514f123569.png

 We encapsulate it into a function to facilitate the creation of a linked list of n nodes .

create node

SLTNode* CreateSList(int n)
{
	SLTNode* phead = nullptr, *ptail = nullptr;
    //int x=0;
	for (int i = 0; i < n; i++)
	{
        //sacnf("%d",&x);
		SLTNode* newnode = BuySLTNode(i);//新节点
		if (phead == nullptr)
		{
			phead = ptail = newnode;
		}
		else 
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}

The purpose of phead recording the head is to find the starting position of the pointer when returning . ptail is responsible for moving and updating to the next node. After n nodes are created, the next of ptail points to empty .

d504e1cf07e84c298759d2cb95ab18e3.pngSuccessfully linked! Although phead will also be destroyed, we use the returned phead pointer to pass to plist to find the head of the linked list.

print linked list

For the print function, we do n't need to record the position of the head pointer , because it just reads the linked list, and its scope ends after printing the linked list.

void Print(SLTNode* plist)
{
	//SLTNode* cur = plist;
	while (plist != nullptr)
	{
		printf("%d->", plist->data);
		plist = plist->next;
	}
	printf("NULL");//方便判空
}

No need to add assert because the empty linked list can also be printed 

9095728ca88644adbf5e2d0cc52a1ea8.png

At this point, you may feel the characteristics of the singly linked list. It only needs one pointer to connect all nodes, provided that there must be a pointer pointing to its head . The sequence table is a structure with continuous storage space , which requires size and capacity to record data.

tail plug

Let's see what's wrong with the following code:

 72b7903ee0104347b754428c8b5ad3f1.png

 operation result:

9fde505e1d1e4233b3c47aa966f74144.png

Hey, why is 10 not inserted? Is there a possibility that it is not linked at all.

 dada47e0e3a04d2d89ed6723ba554f76.png

It can be seen that the wrong program takes the following path, assigning a value when the tail is empty , resulting in no link at all, so we should write it like this:

f63b31e2024042478456e5617d78edd7.png

Inserted successfully! 

If this is the case: a dereference to a null pointer happens when phead is null, can we handle it with an assertion? Of course not, when the linked list is empty, we can also insert data, then we can only deal with it according to the situation.

void SLTPushBack(SLTNode* phead, SLTDataType  x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (phead == nullptr)
	{
		phead = newnode;
	}
	else
	{
		SLTNode* tail = phead;//初始位置
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

a1ec0b45643b4fe4b68423f98439985b.png

 What's going on here? Where does the data we insert go? Let's use a picture to explain.


cf109ab379734fd98f8171923c6b8c00.png

        The phead is a copy of the plist, that is to say, because the plist is empty at this time , and the passed phead cannot traverse the plist and insert the data into the tail, the space applied on the heap will not affect the plist , and the phead is destroyed at the end of the function, causing memory leak .

        The correct way is to pass the address of the plist and receive it with a secondary pointer to achieve the effect of modifying the plist.

void SLTPushBack(SLTNode** pphead, SLTDataType  x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == nullptr)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;//实际是接收plist
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

41550b04bad64e1d96a9c9089328d219.png

In the previous test, it was possible not to pass the second-level pointer because we changed the structure , but now we are changing the first-level pointer , so we pass the second-level pointer uniformly to avoid future troubles.

 tail delete

void SLTPopBack(SLTNode* phead)
{
    assert(phead);
    SLTNode* ptail = phead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	free(ptail);
    ptail = nullptr;
}

Is there any problem with this code? If you think so, you are very wrong. Because ptail is a local variable, which is a copy of the actual parameter , if there is only one data left, setting it empty in the scope will not affect phead. If there is more than one data, the next of the previous position should be blank.

We provide the following two methods for this purpose:

void SLTPopBack(SLTNode* phead)
{
//方法1
	SLTNode* prev = nullptr;
    SLTNode* tail = phead;
    while(tail->next)
    {
        prev = tail;
        tail = tail->next;    
    }
    free(tail);
    prev->next = nullptr;

}
//方法2
  SLTNode* tail = phead;
while(tail->next->next)
{
     tail = tail->next;    
}
free(tail->next);
tail->next = nullptr;
}

359e2f9f5075455baf02dab4e8c33311.png

One is to keep the previous one of the last data , and the other is to find the last two positions , pay attention to judge whether the next is empty ( when there is only one data ). Both methods have to deal with the case of  only one data .

 operation result:

1eae612fe3c24f168df145e54a0d82bf.png

At this point, will anyone think that the program is no longer a problem, of course not.

What should I do if the deletion is empty? What should I do  if tail->next is empty in the two methods?

Based on various situations, we still have to use secondary pointers to pass parameters to deal with this special situation and avoid the situation of wild pointers .

void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
			//方法一
	//SLTNode* prev = nullptr;
	//SLTNode* tail = *pphead;
	//while (tail->next)
	//{
	//	prev = tail;//更新
	//	tail = tail->next;
	//}
	//free(prev->next);//free(tail);
	//prev->next = NULL; 
	}
}

It should be noted that because the second-level pointer is passed, it cannot be traversed directly by the head (modified the pointer) , and it is better to use the new pointer to traverse the linked list in the future.

277d008ff1344e5b9f53c71ce01aa220.png Here we deliberately deleted it one more time, as you can see, the program can run perfectly.

 plug

The implementation of plug-in is very simple, and when the linked list is empty, he can also insert it smoothly.

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

Let me briefly mention here that some programs can also pass a first-level pointer because it records a return value and is received, but if it is received frequently, it will be complicated and redundant, so we use the second-level parameter passing method. 

head delete

It is also very simple to delete the head, just save the address of the next space where it is stored before deleting .

void SLTPopFront(SLTNode** pphead)//头删
{
	assert(*pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

It can be seen that the process of head-plug deletion is very simple, which is also the advantage of the single-linked list. It does not have the traversal and complicated judgment conditions like tail-plug deletion.

look up

Searching is the same as printing data, you only need to pass the first-level pointer and the data to be searched, and there is no need to judge empty.

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return nullptr;
}

d5c619994cae45b5aa828251d8e35ac2.png

 insert

The insertion of a single linked list is divided into inserting before the pos position and inserting after the pos position . Generally, we use inserting after the pos position. 

void SListInsertAfter(SLTNode* pos, SLTDataType x)//插入pos之前
{
	assert(pos);//找不到不插入
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

 Pay attention to the insertion order. If you do not save the position of pos->next or let it point to the new node first, it will cause an infinite loop.

1b35bd28b2174a7c9df41d44b26c003d.png

Why not use a secondary pointer here?

Because we didn't change the pointer position, we just changed the structure .

The pos position here is located by the lookup function .

a4ddd3e3fc5e491490def4a455b437a8.png

Now think about how to insert it in pos?

Because it is pos insertion, our traversal condition is to use the next of the new pointer to traverse instead of itself.

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//可能改变指针
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead,x);//等价于头插
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

Some people ask why a tail plug is not reused after inserting after pos?

Because the tail insertion needs to be traversed again, the time complexity becomes O(n^2)

Note that when we pass pphead into the head plug, we pass the secondary pointer , and the secondary pointer can change the primary pointer, which is equivalent to passing the address of the primary pointer .

7f3afab192a74147b4ab9be895a29bb3.png

 delete

delete after pos

After pos, delete the next node that needs to save pos, then connect , and finally release . (Otherwise pos->next will become a wild pointer).

7783c8ef129148ad9d64e2505c99a439.png

void SLTEraseAfter(SLTNode* pos)//pos后删除
{
	assert(pos);
	assert(pos->next);//最后一个节点后面没有数据
	SLTNode* eraseNode = pos->next;
	pos->next = pos->next->next;
	free(eraseNode);
}

pos position delete

This involves a pointer change so a secondary pointer is passed. We can choose to delete by retaining the previous position , and delete the head if there is only one data.

void SLTErase(SLTNode** pphead, SLTNode* pos)//删除
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);//头删
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

Asserting pos here can achieve the function of asserting pphead at the same time ( pos is also empty when the linked list is empty ).

 test:d11b7ecbde4e4bd597df42a51734149a.png

 freed

The release of the singly linked list cannot be released directly like the sequential table. When we apply for space, we apply for it one at a time, and the release must be released one at a time. Because we want to null the head pointer, we pass the secondary pointer.

void SLTDestroy(SLTNode** pphead)//释放
{
	SLTNode* cur = *pphead;//最好不要用头进行遍历
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = nullptr;//关键置空
}

        Pay attention to save the address of the next node before deleting , and leave it empty after the end. Because it may cause harm if other access operations are not made empty after performing the release operation on the upper stack frame , and the pointers defined in the function body will be destroyed when they go out of scope, so don't care about these pointers having this problem.

full code

//.cpp
#include"SList.h"
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//节点
	if(newnode == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = nullptr;
	return newnode;
}
SLTNode* CreateSList(int n)
{
	SLTNode* phead = nullptr, *ptail = nullptr;
	//int x=0;
	for (int i = 0; i < n; i++)
	{
		//sacnf("%d",&x);
		SLTNode* newnode = BuySLTNode(i);
		if (phead == nullptr)
		{
			phead = ptail = newnode;
		}
		else 
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}
void SLTPushBack(SLTNode** pphead, SLTDataType  x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == nullptr)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;//实际是接收plist
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		//方法一
	//SLTNode* prev = nullptr;
	//SLTNode* tail = *pphead;
	//while (tail->next)
	//{
	//	prev = tail;//更新
	//	tail = tail->next;
	//}
	//free(prev->next);//free(tail);
	//prev->next = NULL; 
	}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
void SLTPopFront(SLTNode** pphead)//头删
{
	assert(*pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return nullptr;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead,x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
void SLTEraseAfter(SLTNode* pos)//pos后删除
{
	assert(pos);
	assert(pos->next);//最后一个节点后面没有数据
	SLTNode* eraseNode = pos->next;
	pos->next = pos->next->next;
	free(eraseNode);
}
void SLTErase(SLTNode** pphead, SLTNode* pos)//删除
{
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);//头删
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}
void Print(SLTNode* plist)
{
	//SLTNode* cur = plist;
	while (plist != nullptr)
	{
		//printf("[%d|%p] ", plist->data, plist->next);
		printf("%d->", plist->data);
		plist = plist->next;
	}
	printf("NULL\n");
}
void SLTDestroy(SLTNode** pphead)//释放
{
	SLTNode* cur = *pphead;//最好不要用头进行遍历
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = nullptr;
}
//.h
#pragma once
#include<stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
SLTNode* BuySLTNode(SLTDataType x);
SLTNode* CreateSList(int n);
void Print(SLTNode* plist);
void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插
//void SLTPushFront();
void SLTPopBack(SLTNode** pphead);//尾删
void SLTPopFront(SLTNode** pphead);//头删
void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插
SLTNode* SLTFind(SLTNode* phead,SLTDataType x);//查找
void SListInsertAfter(SLTNode* pos, SLTDataType x);//插入pos之前
// 在pos之前插入x
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTEraseAfter(SLTNode* pos);//pos后删除
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除
void SLTDestroy(SLTNode** pphead);//释放

Guess you like

Origin blog.csdn.net/dwededewde/article/details/131138741