C/C++ data structure: [Gift demonstration] Introduction and implementation of the structure of the singly linked list

linked list

The concept and structure of linked list

conceptual understanding

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 .

Knowledge points:

  1. The linked list is logically continuous, but physically not necessarily continuous;
  2. The linking method of the linked list is the order of the linked list realized by the pointer link between the data;
  3. Because it is non-sequential and has no subscript guidance, search methods such as binary search and bubble sort cannot be used.

structural understanding

Take singly linked list as an example

image-20220207190801482

Knowledge points:

  1. As can be seen from the above figure, the chain structure is logically continuous, but not necessarily physically continuous;
  2. Each node must also store address information indicating its successor node (next) while storing each node value (data);
  3. Nodes in reality are generally applied from the heap;
  4. Because the space is applied from the heap, and the heap is allocated according to a certain strategy, the space applied for twice may be continuous or discontinuous.

Classification of linked lists

1. One-way or two-way

image-20220209145756546

2. To lead or not to lead

image-20220209145841249

3. Circular and non-cyclical

image-20220401180304343

Although there are so many linked list structures, the following two structures are most commonly used in practice:

image-20220209150003808

  1. Headless one-way non-circular linked list: simple structure , generally not used to store data alone. In practice, it is more of a substructure of other data structures , such as hash buckets, adjacency lists of graphs, and so on. In addition, this structure appears a lot in written test interviews .
  2. Leading two-way circular linked list: the most complex structure , generally used to store data separately. The linked list data structure used in practice is a two-way circular linked list with the lead. In addition, although this structure is complex, after using the code to implement it, you will find that the structure will bring many advantages, and the implementation is simple. We will know when we implement the code later.

Single list

Definition: Node Structure

┌───┬───┐

│data │next │

└───┴───┘

data field --> the data field that stores the node value

next field --> pointer field to store the address (position) of the direct successor of the node;

The linked list links the n nodes of the linked list together in its logical order through the pointer field of each node, and the linked list with only one pointer field for each node is called a single linked list (Single Linked List).

Singly linked list, as the name suggests—singly linked list, there is only one running direction of the singly linked list, the linked list node stores the data of the current node and the address of the next node, and the end of the linked list is a NULL pointer.

Reference Code

C language

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

typedef int SLTDataType; 

typedef struct SListNode 		// 单链表结点结构
{
    
    
	SLTDateType data;  			// 单链表数据信息
	struct SListNode* next;		// 当前结点的后继结点信息
}SLTNode;

void IniSLTNode()
{
    
    
    SLTNode* plist = NULL;      // 初始化单链表
    
}

Java

class ListNode {
    
    
    public int val;
    public ListNode next;

    public ListNode(int val) {
    
    
        this.val = val;
    }
}

public class MyLinkedList {
    
    

    public ListNode head;							//链表的头引用

    public void IniSLTNode() 
    {
    
    
        ListNode listNode1 = new ListNode(12);		// 链表插入12
        ListNode listNode2 = new ListNode(23);		// 链表插入23
        ListNode listNode3 = new ListNode(34);		// 链表插入34
        ListNode listNode4 = new ListNode(45);		// 链表插入45
        ListNode listNode5 = new ListNode(56);		// 链表插入56
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next = listNode5;
        this.head = listNode1;
    }
}

Output: traverse the output linked list

Design ideas:

Because the tail pointer of the singly linked list is empty (NULL) by default, then when we need to traverse and output the entire linked list, we only need to judge whether it has reached the last tail node position to output; when traversing, after the output is completed, we need to adjust the traverse pointer to A pointer to the next node, and so on.

**Time complexity: **O(N)

Reference Code:

C language

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

Logic implementation:

rId44

Insertion: tail insertion method

Design ideas:

  1. First determine whether the head node is empty, because the NULL pointer cannot be used as the head node;
  2. Find the tail node of the linked list, and put the data only when the tail node is found;

Because the single-linked list is traversed forward and one-way, and a node stores the node value and the address of the next node, the logical operation when implementing the tail insertion operation of the single-linked list should be: first determine whether there is data in the linked list , If not, insert the data at the head; when inserting, traverse the head to the point where the tail pointer is NULL, apply for inserting a node into data, and set the position of the successor pointer of the newly applied node to the null pointer NULL. Finally, connect the newly applied node address to the pointer field at the end of the original node to realize the tail insertion of the linked list.

**Time complexity: **O(N)

Reference code :

C language

void SListPushBack(SLTNode** phead, SLTDataType x)
{
    
    
    // 新申请一个结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

    // 如果头节点是空的,就直接将申请到的节点插入
	if (*phead == NULL)
	{
    
    
		*phead = newnode;
	}
	else  // 如果头节点非空,那就去寻找尾结点
	{
    
    
		SLTNode* tail = *phead;  
		while (tail->next != NULL)
		{
    
    
			tail = tail->next;
		}// 找到尾结点后
		tail->next = newnode; // 赋值新的尾结点; 
	}	
}

Logic implementation:

rId48

Insertion: head insertion method

Design ideas:

  1. Apply for new node information, and set the next node address of the newly applied node as the head node of the current linked list;
  2. Set the head node of the current linked list to the applied node information.

*When inserting the tail, we need to consider whether the head node is a null pointer, because a null pointer cannot be used as a head node. When inserting the head, even if the head node is a NULL pointer, it is equivalent to converting the NULL of the head node into a tail pointer, so whether the head node is a null pointer can be ignored when inserting the head .

**Time complexity: **O(1)

Reference Code:

C language

void SListPushFront(SLTNode** phead, SLTDataType x)
{
    
    
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = *phead;
    
	*phead = newnode;
}

Logic implementation:

rId52

Deletion: Tail deletion

Design ideas:

  1. First judge whether the head node of the linked list is empty, if it is empty, it cannot be deleted at the end.
  2. Because the next pointer address of the last node at the end of the singly linked list is a null pointer, then we only need to traverse to find the node whose next pointer address is null.
  3. After finding the tail data, set its address as a null pointer, and set the link pointer of its previous node as null.

Because after deleting the last data, the link pointer of the previous data should be set to empty. The ideal solution is: set a prev pointer to record the address of the current traversal pointer. When the traversal pointer temp reaches the end and deletes the node, prev The position of the pointer is the position of the previous node of the deleted node. At this time, as long as the link pointer of prev points to NULL, the delete operation of the linked list can be completely realized . At the same time, it should be noted here that if the linked list currently has only one node, there is no need to empty the link pointer of the previous node .

**Time complexity: **O(N)

Reference Code:

C language

void SListPopBack(SLTNode** phead)
{
    
    
	if (*phead == NULL) 			// 判断单链表是否为空,如果为空无法进行尾插操作
		return;

	if ((*phead)->next == NULL)		// 如果单链表只有一个数据,只需要释放当前结点即可
	{
    
    
		free(*phead);
		*phead = NULL;
	} 								// 如果单链表长度 > 1
    else 
    {
    
    
        SLTNode* prev = NULL; 		// 记录指针
        SLTNode* temp = *phead;	// 遍历指针
        while (temp->next != NULL)	// 当遍历指针没有到达链表末尾
        {
    
    
            prev = temp;			// 在进入下一次遍历之前,遍历指针记录着遍历指针当前的位置
            temp = temp->next;		// 遍历指针进入下一步
        }

        free(temp);					
        temp = NULL;				// 找到尾结点后,释放尾结点并置为空指针
        prev->next = NULL;			// 记录指针存储着被删除结点的上一结点信息,此时将其结点的链接指针置为空完成尾删。
        
		/* 第二种写法,不需要prev记录指针,连续解引用。
		SLTNode* temp = *phead;
        while (temp->next->next != NULL)
        {
            temp = temp->next;
        }

        free(temp->next);
        temp->next = NULL;
        */
	}
}

Logic implementation:

rId56

Delete: head delete method

Design ideas:

  1. When deleting the head, it is still necessary to consider the case where the linked list is empty.
  2. When deleting the head node, you need to define the second node as the new head node, otherwise the linked list will collapse.

**Time complexity: **O(1)

Reference Code:

C language version

void SListPopFront(SLTNode** phead)
{
    
    
	if (*phead == NULL)					// 考虑链表是否为空
		return;

	SLTNode* next = (*phead)->next;		// 将新的头结点改为第二个结点地址
	free(*phead);						
	*phead = next;						// 释放头结点
}

Logic implementation:

img

Lookup: Lookup a linked list by value

Design ideas:

  1. The idea of ​​searching is the same as that of printing a linked list. When the first element is not found, you only need to traverse it all the time.
  2. Return to the current node if the first element is found, and return NULL if no element is found after walking to the end

**Time complexity: **O(N)

Reference Code:

C language

SLTNode* SListFindElem(SLTNode* phead, SLTDataType x)
{
    
    
	SLTNode* cur = phead;		// 遍历指针cur
	while (cur)					// 只要指针非空
	{
    
    
		if (cur->data == x)		// 找到数据data
			return cur;			// 找到返回结点信息
		else
			cur = cur->next;	// 没找到继续遍历
	}
	return NULL;				// 没找到返回空NULL
}

think:

Can it be rewritten to return the information of the node and the sequence position at the same time? Or find out all the elements in the linked list?

Logic implementation:

rId64

Find: Find a linked list by sequence

Design ideas:

  1. The singly linked list only needs to start from the first node, and traverse the pointer to search down one by one;
  2. Traverse the pointer to traverse until the end of the nth node;

**Time complexity: **O(N)

Reference Code:

C language

SLTNode* SListGetIndexData(SLTNode* phead, int index)
{
    
    
    if (phead == NULL)
        return NULL;
    if (index == 0)
        return phead;

    int count = 1;
    SLTNode* cur = phead;
    // while(cur != NULL)
    while (cur && count < index)
    {
    
    
        cur = cur->next;
        count++;
        // if (count == index) break; 
    }
    return cur;
}

Insert: Insert an element after a node (node ​​method)

The node method means: the incoming parameter is a node of the linked list, you only need to insert after this node

Design ideas:

  1. First determine whether the incoming insertion node is empty, exit if it is empty, otherwise continue;
  2. Point the link pointer of the new node newnode to the link pointer address of the inserted node pos, and connect the back first;
  3. Connect to the front of the new node newnode: point the link pointer of the inserted node to the address of the new node.

**Time complexity: **O(1)

Reference Code:

void SListInsertAfterUsingElem(SLTNode* pos, SLTDataType x)
{
    
    
	if (pos == NULL)
        return;

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    
	newnode->next = pos->next;
	pos->next = newnode;
}

Insert: insert elements after the node (sequence method)

Design ideas:

  1. First determine whether the head node is empty, exit if it is empty, otherwise continue;
  2. Use the traversal pointer prev to go to the insertion position;
  3. Consistent with the node method, point the new node link pointer to the link pointer address of the traversal pointer;
  4. Then point the link pointer of the traversal pointer to the address of the new node;

**Time complexity: **O(N)

Reference Code:

C language

void SListInsertAfterUsingIndex(SLTNode** phead, int index, SLTDateType x)
{
    
    
    if (index == 0 && phead == NULL)
        return;
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;

    SLTNode* prev = *phead;
    for (int count = 0; count < index; count++)
    {
    
    
        prev = prev->next;
        if (prev == NULL)
        {
    
    
            printf("超出链表长度导致了插入失败\n");
            return;
        }
    }
    newnode->next = prev->next;
    prev->next = newnode;
}

Insert: Insert an element before a node (node ​​method)

(Forward insertion is more complicated than backward insertion, but try to be familiar with the idea of ​​forward insertion operation, which is very helpful for the subsequent analysis of C++ STL library)

If you need to insert an element before a node, you need to change the link pointer of the previous node to a new node, and set the link pointer of the new node to the address of the current node . Does this sentence sound familiar? That's right, it is similar to the idea of ​​tail deletion method of linked list (on-site inspection of tail deletion method!!)! But here we don't need to delete elements, the main thing we do is to replace the link pointer of the previous node.

Design ideas:

  1. You can use the previous section to find elements based on node values, and first find out the node information;
  2. Use the traversal pointer to traverse to the previous position before the target node, and then insert a new node and connect to the original node;
  3. If the insertion address is in the head element, it only needs to set the link pointer of the new node to the address of the original head element.

**Time complexity: **O(N)

Reference Code:

void SListInsertBeforeUseElem(SLTNode** phead, SLTNode* pos, SLTDateType x)
{
    
    
    if (phead == NULL)
        return;
    if (pos == NULL)			   		// 确定信息在链表中存在
        return;
    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
	newnode->next = NULL;
    
	if (*pphead == pos)					// 如果插入元素的地址是头元素
	{
    
    
		newnode->next = *phead;
		*phead = newnode;
	}
	else
	{
    
    
		SLTNode* posPrev = *phead;		// 用来找到pos的前一个位置的遍历指针
		while (posPrev->next != pos)	// 找到pos的前一个位置
		{
    
    
			posPrev = posPrev->next;
		}

		posPrev->next = newnode;		// 将前一个位置的链接指针为新结点
		newnode->next = pos;			// 将新结点的链接指针为插入位置
	}
}


Insert: insert elements before the node (sequence method)

Design ideas:

  1. Similar to the method of finding elements of a linked list based on a sequence. When we only need to go to the previous position;
  2. After coming to the previous position of the current sequence, it is consistent with the node method insertion operation;

**Time complexity: **O(N)

Reference Code:

void SListInsertBeforeUseIndex(SLTNode** phead, int index, SLTDateType x)
{
    
    

    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

    if (index == 0)								// 如果插入元素的地址是头元素, 直接插入
    {
    
    
        newnode->next = *phead;
        *phead = newnode;
    }
    else
    {
    
    
        int count = 0;
        SLTNode* posPrev = *phead;				// 遍历指针

        SLTNode* prev = NULL; 					// 记录指针
        while (posPrev->next != NULL)
        {
    
    
            prev = posPrev;			  			// 在进入下一次遍历之前,记录指针会记录着遍历指针当前的位置
            posPrev = posPrev->next;			// 遍历指针进入下一步
            count++;
            if (count == index)
            {
    
    
                prev->next = newnode;			// 将前一个位置的链接指针的指向为新结点
            	newnode->next = posPrev;		// 将新结点的链接指针为插入位置
                return;
            }
        }
        printf("超出链表长度导致了插入失败\n");		// 如果未达到指定位置而已经遍历完成
        return;
    }
}

Delete: Delete the specified node of the linked list (node ​​method)

Design ideas:

  1. If the deleted node is a head node, it needs to be processed separately, because it is a head deletion, you can directly call the previous head deletion function;
  2. If it is a non-head node, what is the deletion logic? To delete a node, you need to find its previous node and point the connection pointer to the next pointer address of the deleted node, and then release the pointer of the node to be deleted to free.

**Time complexity: **O(N)

Reference code :

void SListEraseUsingElem(SLTNode** phead, SLTNode* pos)
{
    
    
	if(phead == NULL || pos == NULL)	// 确定传入合法性
        return;

	if (*phead == pos)					// 头删
	{
    
    
		SListPopFront(phead);
	}
	else
	{
    
    
		SLTNode* prev = *phead;			// 遍历指针prev
		while (prev->next != pos)		
		{
    
    
			prev = prev->next;			// 找到删除结点的上一个结点位置
		}

		prev->next = pos->next;			// 将上一结点的链接指针 指向 删除结点的下一结点地址
		free(pos);						// 释放删除结点
		pos = NULL;						// 这里的pos可以置空也可以不置空,因为pos是一个形参,出了函数就会被栈帧销毁
	}
}

Delete: Delete the specified node of the linked list (sequence method)

Design ideas:

  1. The idea is similar to the node method, except that the sequence method needs to use a loop to find the previous position of the deleted node;
  2. After coming to the previous position of the deletion sequence, start the deletion operation: point the connection pointer to the next pointer address of the deleted node, and then release the pointer of the node to be deleted to free.

**Time complexity: **O(N)

Reference Code:

void SListEraseUsingIndex(SLTNode** phead, int index)
{
    
    
    if (phead == NULL)								// 确定传入合法性
        return;

    if (index == 0)
    {
    
    
        SListPopFront(phead);
    }
    else
    {
    
    
        int count = 0;
        SLTNode* posPrev = *phead;				    // 遍历指针,等会它会指向删除结点

        SLTNode* prev = NULL; 					    // 记录指针,之后会指向删除结点的前一个位置
        while (posPrev->next != NULL)
        {
    
    
            prev = posPrev;			  			    // 在进入下一次遍历之前,记录指针会记录着遍历指针当前的位置
            posPrev = posPrev->next;			    // 遍历指针进入下一步
            count++;
            if (count == index)
            {
    
    
                prev->next = posPrev->next;			// 将前一个位置的链接指针指向删除结点的下一个位置
                free(posPrev);
                posPrev = NULL;						// 思考一下,这里的posPrev要不要置空呢?
                return;
            }
        }
        printf("超出链表长度导致了删除失败\n");      	 // 如果未达到指定位置而已经遍历完成
        return;
    }
}

Delete: delete elements after the node (node ​​method)

Design ideas:

To delete the element after the node, it is necessary to obtain the address of the node at the last 2 sequence positions and link it, and then release it to the following node, so we have the following scheme:

  1. After getting the incoming node, first temporarily copy its next node information next (that is, the deleted node);
  2. Point the next pointer address of the original node to the next node address of next;
  3. Release the deleted node next.

**Time complexity: **O(1)

Reference code :

void SListEraseAfterUsingElem(SLTNode* pos)
{
    
    
    if (pos == NULL || pos -> next == NULL)		// 检查传入合法性
        return;

	SLTNode* next = pos->next;					// next为结点后的元素,这里先拷贝一份出来
	pos->next = next->next;						// 将结点指向删除元素的下一个结点
	free(next);									// 释放被删除的结点
    next = NULL;								// 同样,这里的next是否置空都可以,但出于职业素养我还是置空吧OWO~
}

Delete: delete elements after the node (sequence method)

Design ideas:

The idea is consistent with the node method, while the sequence method only has sequence position information, so we need to loop through to this position;

  1. Additional consideration needs to be given to the processing if the deletion sequence position exceeds the length of the linked list.

Time complexity : O(N)

Reference code :

void SListEraseAfterUsingIndex(SLTNode** phead, int index)
{
    
    
    printf("%d: ", index);
    SLTNode* posPrev = *phead;
    for (int count = 0; count < index; count++)
    {
    
    
        posPrev = posPrev->next;
        if (posPrev == NULL)
        {
    
    
            printf("超出链表长度导致了删除失败\n");   // 如果已经到达尾部仍为到达序列位置时直接退出
            return;
        }
    }
    SListEraseAfterUsingElem(posPrev);			 // 因为遍历指针已经走到尾删的结点处,直接调用结点法偷个小懒OWO吧~
}

Destroy: Destroy the linked list

Design ideas:

Destroying the linked list is consistent with the idea of ​​outputting the linked list, traversing all the way, freeing up space all the way;

  1. Design a record pointer, and record the address of the next node while releasing it;
  2. After releasing a node, the loop continues to enter the next node and release until the end;

**Time complexity: **O(N)

Reference Code:

C language

void SListDestory(SLTNode** pphead)
{
    
    
	if (phead == NULL)
        return;

	SLTNode* cur = *phead;
	while (cur)
	{
    
    
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*phead = NULL;
}

The difference between sequence list and linked list

Sequence table: One white covers one hundred ugly
whites:

  1. spatial continuity;
  2. Random access is supported.

ugly:

  1. The time complexity of insertion and deletion in the middle or front part is O(N);
  2. The cost of capacity expansion is relatively high.

image-20220213012910545

Linked list: one (fat black) destroys all
fat black:

  1. Stored in units of nodes;
  2. Random access is not supported.

all:

  1. The time complexity of inserting and deleting at any position is O(1)
  2. There is no problem of capacity expansion, inserting one opens up a space.

image-20220213013018296

sequence table

advantage

  1. Random access is supported. Algorithms that require random access structures to support are well suited.
  2. Higher cpu cache utilization. ( See the extended reading below for details on cache utilization )

shortcoming:

  1. The insertion and deletion time in the middle of the head is inefficient; O(N)
  2. Continuous physical space, if the space is not enough, it needs to be increased in the future;
  3. Capacity increase has a certain degree of consumption;
  4. In order to avoid frequent capacity expansion, we generally increase the capacity by multiples, and there may be a certain waste of space if it is not used up.

linked list

advantage:

  1. Insertion and deletion at any position is highly efficient. O(1);
  2. Apply for free space on demand.

shortcoming:

  1. Random access is not supported. (access with subscript) means: some sorting, binary search, etc. are not applicable on this structure;
  2. The linked list stores a value and at the same time stores the link pointer, which consumes to a certain extent;
  3. Lower cpu cache utilization.
difference sequence table linked list
storage space must be physically continuous Logically continuous, but not necessarily physically continuous
random access supports O(1) Not supported: O(N)
Insert or delete elements at any position May need to move elements, low efficiency O(N) Just modify the pointer to point to
insert Dynamic sequence table, when the space is not enough, it needs to be expanded no concept of capacity
Application Scenario Efficient storage of elements + frequent access Frequent insertion and deletion at any position
cache utilization high Low

Further reading: What is a CPU cache

You can browse my previous introduction to CPU cache: What is CPU cache?

The single linked list is over~ the meeting is over!

Summarize

A singly linked list implementation and introduction notes are over! ~ Because the relevant knowledge points are not only important, but also very, very many, so the length is longer! Believe that you will be able to gain a lot. If you have any questions or mistakes, welcome to exchange and learn in the comment area!

If you think this series of articles is helpful to you, don't forget to like and follow the author! Your encouragement is my motivation to continue to create, share and add more! May we all meet at the top together. Welcome to the author's official account: "01 Programming Cabin" as a guest! Follow the hut, learn programming without getting lost!

Guess you like

Origin blog.csdn.net/weixin_43654363/article/details/124567659