Data Structure Series Learning (3) - Singly Linked List (Linked_List)

Table of contents

introduction:

study:

Code:

The structure design of the linked list:

Function description:

Function declaration in header file (Linked_List.h):

The specific implementation of the function in the source file (Linked_List.cpp):

1. Initialization function (Init_List):

Insert function group (Insert): 

Question: Does the insertion of the linked list need to be full?

2. Head insertion function (Insert_head):

3. Tail insertion function (Insert_tail):

4. Insert function by position (Insert_pos):

Delete function group (Delete): 

5. Head delete function (Delete_head):

6. Tail delete function (Delete_tail):

7. Delete function by position (Delete_pos):

8. Delete function by value (Delete_val):

Different loops in functions:

9. Search function (Search):

10. Empty judgment function (IsEmpty):

11. Clear function (Clear):

12. Destroy function (Destroy):

13. Print function (Show):

14. Get the length function (Get_length):

test: 

Test insert function group:

Test initialization functions, interpolation functions by position:

Test header plug-in function:

Test tail interpolation function:

Test delete function group:

Test header delete function:

Test tail delete function: 

Test the delete function by position: 

Test the delete-by-value function:

Test clear function:

 Test destroy function:

Test get length function: 

Test lookup function:

Summarize: 

References:


introduction:

Before that, we systematically studied the basic concepts and time complexity in data structure, and implemented the sequence table (Contiguous_List) with code. In the article summary of the study and implementation of the sequence table, we mentioned that the advantage of the sequence table is that You can directly access any element in the sequence table, but the disadvantage is that if you insert or delete at the head or the middle position, the time and space overhead caused by moving the element is relatively large. In this article, the linked list (Linked_List) we will learn is not necessarily continuous in computer memory, and it can also effectively reduce the time and space overhead caused by inserting and deleting elements. For its shortcomings, we will systematically study the linked list and use code to implement it.

Data structure learning directory:

Data Structure Series Learning (1) - An Introduction to Data Structure

Data structure series learning (2) - detailed explanation of sequence table (Contiguous_List)

study:

In order to avoid the huge time and space overhead caused by insertion and deletion, the linear list has another form of expression-linked list. The difference between a linked list and a sequential list is that the linked list is not stored contiguously.

A linked list is a structure whose columns are not necessarily connected in memory. Each structure contains a table element and pointers to structures containing the element's successors. We also call this pointer the Next pointer. The Next pointer of the last unit points to NULL, where NULL is 0.


 

Features: logically adjacent, but not necessarily physically adjacent
. Because the data elements do not require physical continuity, it is the advantage of virtual sequence tables that can be accessed randomly. Single-linked
list: use a group of arbitrary storage units to store linear tables The data elements (this set of data can be continuous or discontinuous.
Therefore, in order to represent the logical relationship between each node An and its successor node An+1, each node not only needs to save its own effective value (numeric field), It is also necessary to save the address of the next node (pointer field))
Singly linked lists are generally divided into two implementation methods: leading node (commonly used), not leading node (relatively troublesome to implement)

Code:

Note: Here we implement a singly linked list without a head node.

The structure design of the linked list:

In the process of learning the linked list, we already know that the linked list contains the data field (data) and the pointer field (next). We should also be consistent with these two things when designing the structure. Here we first redefine the generic type int The type is Elem_type, so the data field in the linked list is naturally the Elemtype type, and the pointer field in the linked list is the pointer of the linked list structure type:

typedef int Elem_type;//给出范型重命名
typedef struct Node{
    Elem_type data;//数据域(保存数据的有效值)
    struct Node* next;//指针域(保存有效节点,也就是node的节点)
}Node,*PNode;//将结构体重命名为Node,结构体类型的指针变量命名为PNode

Function description:

To realize the linked list, the first thing to be clear is what the linked list we are about to implement can do, that is, what functions the linked list should have. The following are the functions that the linked list should have:

Initialization function (Init_List)

Head insertion function (Insert_head)

Tail insert function (Insert_tail)

Insert function by position (Insert_pos)

Head delete function (Delete_head)

Tail delete function (Delete_tail)

Delete function by position (Delete_pos)

Delete function by value (Delete_val)

Search function (Search)

Empty judgment function (IsEmpty)

Clear function (Clear)

Destroy function (Destroy)

Print function (Show)

Get the effective number (length) function (Get_length)

Function declaration in header file (Linked_List.h):

//1 初始化
void Init_List(struct Node* plist);
//2 头插
bool Insert_head(struct Node* plist,Elem_type val);
//3 尾插
bool Insert_tail(struct Node* plist,Elem_type val);
//4 按位置插
bool Insert_pos(struct Node* plist,int pos,Elem_type val);
//5 头删
bool Delete_head(struct Node* plist);
//6 尾删
bool Delete_tail(struct Node* plist);
//7 按位置删
bool Delete_pos(struct Node* plist,int pos);
//8 按值删
bool Delete_val(struct Node* plist,Elem_type val);
//9 查找 返回的是查找到的这个节点的地址
struct Node* Search(struct Node* plist,Elem_type val);
//10 判空
bool IsEmpty(struct Node* plist);
//11 清空
bool Clear(struct Node* plist);
//12 销毁
bool destroy(struct Node* plist);
//13 打印
void Show(struct Node* plist);
//14 获取有效个数(长度)
int Get_length(struct Node* plist);

The specific implementation of the function in the source file (Linked_List.cpp):

1. Initialization function (Init_List):

The first thing we need to do to initialize the linked list is to set the pointer field of plist to NULL:

code:

void Init_List(struct Node* plist)
{
    //1 判断plist是否为空地址(安全性处理)
    assert(plist != NULL);
    //2 对于plist指向的头节点里面的每一个成员变量进行赋值
    plist->next = nullptr;//将链表中的头节点的next赋值为空
}

Insert function group (Insert): 

Before writing the insertion function group, we should first know how the insertion operation (head insertion, tail insertion, and position insertion) in the linked list is completed. Here we give a picture:

Let's take the linked list structure diagram drawn above as an example. This is the initial state of the linked list, and pnewnode represents the new node to be inserted:

We know that the data field of the initial node in the singly linked list without the head node is empty, and the Next pointer points to the data field of the next node, so if I want to perform an insert operation, I manipulate the data field that should point to the next node. The next pointer points to the data field of the pnewnode node at this time, and then manipulates the next pointer of the pnewnode to point to the data field of the next node to form a ring structure, as shown in the figure:

This is the operation demonstration of inserting in the linked list. 

Question: Does the insertion of the linked list need to be full?

Before writing the head insertion function, let's discuss a question, does the insertion of the linked list need to be full?

As mentioned in the previous article on the sequence table, the sequence table is a whole block of continuous memory that we apply for. The sequence table is continuous in both storage structure and physics. Therefore, in the insertion and deletion operations of the sequence table, we The first thing to do is to judge whether the sequence table is full. If it is full, perform the expansion operation and then add or delete. If it is not full, directly add or delete the operation. Or all the following elements are moved, and the time and space overhead is relatively large.

Compared with the sequential list, the linked list itself is another form of expression of the linear list. The linked list is continuous in storage structure, but not continuous in physics. There are two members in the linked list, one is the data field for storing data and the other is the array field, which stores the address of the next node. This is why the physical space in the linked list is not continuous but the storage structure is continuous. The nodes in the linked list are obtained through the application of the malloc function step by step. After the node is applied, the purpose of adding a new node is achieved by assigning a value to the data field of the new node and modifying the pointer field, so as long as the heap area There is still enough space, and the linked list does not need to be full.

2. Head insertion function (Insert_head):

If we want to insert data, we must store the data in the data field of the new node of the linked list, and the new node of the linked list needs to be applied for in the heap area through the malloc function, so the general process can be expressed as: make the pointer safe For permanent processing, apply for the memory space of the new node of pnewnode in the heap area. After the application is completed, store the data in the data field of the new node, modify the next field of pnewnode, and then update the head node of the plist:

code:  

bool Insert_head(struct Node* plist,Elem_type val)
{
    //1 安全性处理
    assert(plist != NULL);
    //2 购买新节点
    struct Node* pnewnode = (struct Node*)malloc(1 * sizeof(struct Node));
    // 安全性处理
    assert(pnewnode != NULL);
    //3 插入数据
    pnewnode->data = val;
    //4 插入操作 将新节点的next指向前驱节点的next
    pnewnode->next = plist->next;
    //将新节点的地址赋值给前驱节点的next域(这里的前驱节点就是原先的头节点plist)
    plist->next = pnewnode;
    return true;
}

3. Tail insertion function (Insert_tail):

Similar to the write head insertion function, when we write the tail insertion function, we also need to apply for a new node first, assign the value we want to insert to the data field of the new node, and modify the next of the original end node to save the new node address, and then assign the next value of the new node to empty:

code:

bool Insert_tail(struct Node* plist,Elem_type val)
{
    //1 安全性处理
    assert(plist != nullptr);
    //2 购买新节点
    struct Node* pnewnode = (struct Node*)malloc(1 * sizeof(struct Node));
    assert(pnewnode != nullptr);
    pnewnode->data = val;
    //3 使用一个新指针指向链表的头部
    pnewnode->next = nullptr;
    //4 申请新指针
    struct Node *p = plist;
    //5 通过for循环找到需要插入的位置
    //注:不需要前驱的函数,例如查找、判断有效值的个数,打印函数直接使用指向头节点的for循环即可
    //但是这里我们是需要前驱的,所以使用指向头节点的for循环进行遍历
    for(;p->next!= nullptr;p=p->next){
        p->next = pnewnode;
    }
    pnewnode->next = p->next;
    p->next = pnewnode;
    return true;
}

4. Insert function by position (Insert_pos):

We default pos = 0 as head insertion. When pos is equal to n, the new node is inserted after the nth effective node after the head node. For example, we want to insert a new node with address 600 at pos = 2. First What we have to do is to apply for a new node in the heap area (using the malloc function), use a new pointer to point to the head node of the singly linked list, define a loop, the loop condition is i < pos, and find the position to be inserted, because our newly defined pointer p points to the head node, so the next field of the p node traversed to the position of pos stores the address of the next node, we assign this address to the next field of the new node, and then we give the address of the new node to p The pointer points to the next field of the node so as to realize the addition operation of the node.

As shown in the picture:

code:

bool Insert_pos(struct Node* plist,int pos,Elem_type val)
{
    //1 安全性处理
    assert(plist != nullptr && pos >= 0 && pos <= Get_length(plist));
    //2 购买新节点
    struct Node* pnewnode = (struct Node *)malloc(1 * sizeof(struct Node));
    //3 找到待插入位置
    struct Node *p = plist;
    for(int i = 0;i < pos;i++){
        p = p->next;
    }
    pnewnode->next = p->next;
    p->next = pnewnode;
    return true;
}

Delete function group (Delete): 

5. Head delete function (Delete_head):

First of all, it needs to be clear that what we delete is the first valid valid node after the head node. Our first step is to judge the emptyness of the singly linked list, and return false if it is empty. Define a new pointer to point to the node to be deleted, because the last node we deleted is the head node, which itself has a plist pointer, so we don't need to define another pointer. Then we perform cross-pointing operations, that is, make the pointer of the head node directly point to the second valid node after the head node, because the p pointer we defined points to the first valid node, so the next field of p stores the next For the address of a node, we assign the address saved in the next field of p to the next field of plist, that is, replace the next field of plist that originally saved the address of the first valid node with the address of the second valid node. In this way, our cross pointing operation can be completed.

As shown in the picture:

code:

bool Delete_head(struct Node* plist)
{
    //1 安全性处理
    assert(plist != NULL);
    //2 判空
    if(IsEmpty(plist)){
        return false;
    }
    //3 申请一个新指针p指向待删除节点
    struct Node *p = plist->next;
    //4 申请一个新指针q指向待删除节点前面的节点(前驱)
//    struct Node *q = plist; 因为头删的上一个节点就是头节点
    //5跨越指向(让待删除节点上一个节点的next域保存下一个节点的地址)
    plist->next = p->next;
    //释放待删除节点
    free(p);
    return true;
}

6. Tail delete function (Delete_tail):

What we delete is the last node in the linked list. The tail deletion function is similar to the idea of ​​head deletion, and both require us to locate the node to be deleted first. We can first apply for a pointer p to point to the head node, and traverse from the head node to the penultimate node (the loop condition is that the next field of p is not empty), we then apply for a pointer q to point to the head node, and traverse from the head node to the end A node (the loop condition is that the next field of the next field of q is not empty or the next field of q is not p), now we can assign the empty address saved in the next field of q to the next field of p.

As shown in the picture:

code:

bool Delete_tail(struct Node* plist)
{
    //1 安全性处理
    assert(plist != NULL);
    //2 判空
    if(IsEmpty(plist)){
        return false;
    }
    struct Node *p = plist;
    for(;p->next != nullptr;p = p->next);//此时for循环执行结束,p指向尾节点
    struct Node *q = plist;
//    for(;q->next->next = nullptr;q = q->next);
    for(;q->next != p;q = q->next);
    q->next = p->next;
    free(p);
    return true;
}

7. Delete function by position (Delete_pos):

We default to delete the head when pos is equal to 0, so when pos = n, we delete the nth node after the U-turn node. Firstly, check for emptyness, and return false if the linked list is empty. Define a new pointer q to point to the head node, traverse from the head node to find the previous node of the node to be deleted, use the pointer to save the next field of this node (the next field here is equivalent to the address of the node to be deleted), and then define a new pointer p points to the head node, and assigns the address of the node to be deleted to p. At this time, the next field of p is equivalent to the next node of the node to be deleted, and then we assign the next field of p to the next field of q, so that we This completes the pointing across.

As shown in the figure: code:

bool Delete_pos(struct Node* plist,int pos)
{
    //1 安全性处理
    assert(plist != NULL && pos >= 0 && pos != Get_length(plist));
    if(IsEmpty(plist)){
        return false;
    }
    struct Node *q = plist;
    for(int i = 0;i < pos;i++){
        q = q->next;
    }
    struct Node *p = plist;
    p = q->next;
    q->next = p->next;
    free(q);
    return true;
}

8. Delete function by value (Delete_val):

First perform the null judgment operation, if the singly linked list is empty, it will return false, define a new pointer p to save the address returned by the search function, if the search function does not find the returned empty address, it will also return false. Define a new pointer q to point to the head node, and start traversing until the node with the data to be deleted is found. At this time, the next field of p is the address of a node behind the node to be deleted, and the address of this node is assigned to the next field of q. In this way, We have achieved cross-pointing.

code:

bool Delete_val(struct Node* plist,Elem_type val)
{
    //1 安全性处理
    assert(plist != nullptr);
    if(IsEmpty(plist)){
        return false;
    }
    struct Node *p = Search(plist,val);
    if(p == nullptr){
        return false;
    }
    struct Node *q = plist;
    for(; q->next != p;q = q->next);
    q->next = p->next;
    free(p);
    return true;
}

Different loops in functions:

When we wrote the insertion and deletion functions before, if we insert and delete by position or delete by value, we need to find the target position through loops, and the operations in the insertion or deletion functions usually need to use the predecessor of the node , so when defining a new pointer, we usually use the new pointer to save the address of the head node. But in the lookup function, we don't need to use the predecessor of the node, we only need to read the data field of the node, so we use the newly defined pointer to save the first valid node.

Therefore, the loops used in the insertion and deletion function group and the search and print function group are different.

The former for loop needs a precursor, expressed in code that is:

struct Node *p = plist;//plist为头节点的地址
for(struct Node *p;p->next != nullptr;p = p->next)

The latter's for loop does not require a precursor, which is expressed in code:

struct Node *p = plist -> next;
for(struct Node *p;p != nullptr;p = p->next)

9. Search function (Search):

First, we judge the singly linked list to be empty, if it is empty, return false, use a new pointer to point to the first valid node after the head node of the singly linked list, define a loop, and the loop condition is that p is not empty (because the last node next is empty), when the value in the data field pointed to by p matches the value to be found, return the address of p, that is, the address of the value to be found.

code:

struct Node* Search(struct Node* plist,Elem_type val)
{
    //1 安全性处理
    assert(plist != NULL);
    if(IsEmpty(plist)){
        return NULL;
    }
    for(struct Node *p = plist ->next;p != nullptr;p = p->next){
        if(p->data == val){
            return p;
        }
    }
    return nullptr;
}

10. Empty judgment function (IsEmpty):

The empty judgment function is easy to understand. We know that the data field in the head node in the singly linked list does not store any value, and next stores the address of the next node. If the next field of the head node in the singly linked list is empty, It also means that the singly linked list is empty.

code: 

bool IsEmpty(struct Node* plist)
{
    //2 当头节点的next指针指向空,则代表单链表为空
    return plist->next == nullptr;
}

11. Clear function (Clear):

In the linked list, the concepts of emptying and destroying are similar, and we can call the destroying function in the clearing function.

code:

bool Clear(struct Node* plist)
{
    destroy(plist);
    return true;
}

12. Destroy function (Destroy):

Here we implement the destruction function by deleting infinite headers until it satisfies the null function, and then return true.

bool destroy(struct Node* plist)
{
    //1 安全性处理
    while(!IsEmpty(plist)){
        Delete_head(plist);
    }
    return true;
}

13. Print function (Show):

If we need to print the valid values ​​in the linked list, first we define a new pointer to point to the first valid node after the head node, define a loop, the loop condition is that p is not empty, and every time a node is looped to, the data field of this node will be The value in is printed.

code:

void Show(struct Node* plist)
{
    //1 安全性处理
    assert(plist != NULL);
    //2 定义新指针指向链表
    struct Node *p = plist->next;
    for(;p!= nullptr;p = p->next){
        printf("%3d", p->data);
    }
}

14. Get the length function (Get_length):

int Get_length(struct Node* plist)
{
    assert(plist != nullptr);
    int count = 0;
    struct Node *p = plist->next;
    for(;p!= nullptr;p = p->next){
        count++;
    }
    return count;
}

test: 

Test insert function group:

Test initialization functions, interpolation functions by position:

We create a linked list named head, and create 10 spaces to insert values ​​into it (1~10):

#include<cstdio>
#include<cassert>
#include "Linked_List.h"
int main()
{
    struct Node head;
    Init_List(&head);//头节点
    for(int i = 0;i < 10;i++){
        Insert_pos(&head,i,i + 1);
    }
    printf("原始数据为:");
    Show(&head);
    return 0;
}

operation result:

Test header plug-in function:

Insert_head(&head,100);
printf("\n变更后的数据为:");
Show(&head);

operation result: 

Test tail interpolation function:

    Insert_tail(&head,100);
    printf("\n变更后的数据为:");
    Show(&head);

operation result:

Test delete function group:

Test header delete function:

    Delete_head(&head);
    printf("\n变更后的数据为:");
    Show(&head);

operation result:

Test tail delete function: 

    Delete_tail(&head);
    printf("\n变更后的数据为:");
    Show(&head);

operation result:

Test the delete function by position: 

    Delete_pos(&head,2);
    printf("\n变更后的数据为:");
    Show(&head);

Test the delete-by-value function:

    Delete_val(&head,2);
    printf("\n变更后的数据为:");
    Show(&head);

operation result:

Test clear function:

    Clear(&head);
    printf("\n变更后的数据为:");
    Show(&head);

operation result: 

 Test destroy function:

    destroy(&head);
    printf("\n变更后的数据为:");
    Show(&head);

operation result:

Test get length function: 

    int len = Get_length(&head);
    printf("\n链表的有效长度为:%d\n",len);

operation result:

Test lookup function:

Let's say the value we're looking for in the linked list is 3:

    struct Node *p = Search(&head,3);
    printf("\n元素在链表中的地址为:%p\n",p);

operation result:

  

But if we look for a 100 that was not in the original linked list:

    struct Node *p = Search(&head,100);
    printf("\n元素在链表中的地址为:%p\n",p);

As shown in the figure, the return value is the null value (nullptr) we originally set.

Summarize: 

Comparison between sequence list and linked list:

Underlying implementation: the sequence table is a container for continuous storage, which allocates space on the heap; the linked list is dynamic, and also allocates space on the heap.

Space utilization: If the sequence table purchases space in advance, there is a high probability that the space will not be full, and the space utilization rate is low; inserting a node into a linked list to buy a node will not cause waste, so the space utilization rate is high.

Find elements: the time complexity of searching elements in the sequence table is O(n), and the separate random access space is O(1) because representatives can be used; the search element of the linked list is O(n);

Insertion and deletion: If the sequence table is tail insertion and tail deletion, the time complexity is O(1), but if it is head insertion, insertion by position, head deletion, deletion by position, and deletion by value, the time complexity is O(n) ;If the linked list is head insertion, the time complexity is O(1), if it is tail insertion, the time complexity of finding a suitable position is O(n), and the time complexity of insertion operation is O(1), if it is middle position insertion Or delete, the time complexity of finding the previous node is O(n), and the time complexity of deletion and insertion operations is O(1).

Because inserting and deleting elements in the sequence table requires moving elements, only the time complexity of the tail operation is O(1), and the others are O(n). The nodes of the singly linked list are linked by pointers, and there is no need to move elements, so the time complexity of insertion and deletion is O(1).

Linked list is another form of expression of linear list. Compared with sequential list, it has obvious advantages and disadvantages. When we use these two data structures, we should understand their principles and use them in different scenarios according to their characteristics. Inside to distinguish and use. The linked list is more abstract than the sequential list, and it is also our accurate use of the memory structure. Understanding and proficiently using the data structure of the linked list is a skill we must have as programmers. Will continue to follow up.

Applicable scenarios for sequential lists and singly linked lists:

1. If the number of nodes can be roughly estimated, use a sequence table, and if it cannot be estimated, use a singly linked list;

2. If insert and delete operations are often used, use a singly linked list because there is no need to move elements;

3. If you only use tail deletion and tail insertion, you can also consider the sequence table;

3. If you often need to access elements, you can consider using a sequence table, because the sequence table can directly access elements through subscripts.

References:

Yan Weimin - "Data Structure (C Language Edition)" - Tsinghua University Press

Mark·Allen·Weiss - "Data Structure and Algorithm Analysis (C Language Description)" - Mechanical Industry Press

Guess you like

Origin blog.csdn.net/weixin_45571585/article/details/127479607