链表(LinkList)数据结构的基本操作实现详解,纯干货

Posted SuchABigBug

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表(LinkList)数据结构的基本操作实现详解,纯干货相关的知识,希望对你有一定的参考价值。

一、前言

本篇主要实现链表的一些基本操作,链表从逻辑结构上说我们自己想象出来的,也就是我们所画的phead➡️ NULL,而从物理结构上是不存在在的,严格意义上说是当前节点存储着下一个节点的物理地址

逻辑结构(为了便于加强理解):

物理结构(实际存储):

什么是带哨兵卫的头?

图中的lessHead和greatHead不进行任何有效数据的存储,而是仅用于存放下一个节点的地址,这就是哨兵头节点

diff顺序表链表
存储空间上物理上一定连续逻辑上是连续的,物理上不一定
随机访问通过下标index访问, 时间上O(1)不支持,时间上O(N)
任意位置插入或删除元素可能需要挪动元素,效率低O(N)只需修改指针指向 O(1)
插入动态顺序表,空间不够需扩容没有容量概念,直接加入新节点并链接即可
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率

为什么数组的缓存利用率会比链表高?

数组:
cpu执行指令运算要访问内存,先要取一个地址如0x10的数据,拿0x10去缓存L1中找,发现没有(不命中),再去L2缓存找,找到了!这个时候会把主存中这个地址开始的一段空间都读进L1缓存,下一次访问0x14,0x18…等就会命中,因为一次性都读进了缓存

如果是链表:
cpu要执行指令,访问内存,先去第一个节点0x60,不命中
取第二个0x80,不命中…
并且还会造成一定的缓存污染,因为和数组一样也会取附近的地址,而这些地址是没用的,污染了缓存

下图很好的说明了CPU是如何高效访问的

二、整体设计框架


实现链表和上一篇的顺序表(SequenceList)数据结构的基本操作实现详解框架一样,我们可以分为三个文件,这样可读性更高,便于调试

test.c 用于函数调用、调试
linkList.h 只用于函数声明
linkList.c 用于函数实现

三、函数实现

我们先看下头文件,这里创建的链表结构成员相比顺序表只有两个,第一个成员用于存储当前数据,第二个成员存储的是下一个linkListNode的地址,物理上结构是这样的,在逻辑上位了便于理解我们可以用这个符号------>代表链子,将前一个节点和后一个节点相连来建立关系。

#ifndef linkList_h
#define linkList_h

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

typedef int DataType;

typedef struct linkList{
    DataType data;
    struct linkList* next;
}LST;

LST* createLSTNode(DataType x);
void linkListPrint(LST* phead);

void linkListPushBack(LST** pphead, DataType x);
void linkListDestory(LST** pphead);

#endif /* linkList_h */

1. createLSTNode

先来实现第一个函数,我们在创建这个函数前需要想明白一件事就是,是用哨兵卫作为head,还是当前第一个就是存储有效数据的头节点呢? 这里我就用当前节点作为第一个有效节点

由于后续会频繁的创建新节点,我们就写成一个函数,直接调用就可以了

LST* createLSTNode(DataType x){
    LST* newnode = (LST*)malloc(sizeof(LST));
    newnode->data = x;
    newnode->next = NULL;
    return newnode;
}

2. linkListPushBack

第一个参数为二级指针,这里需要注意一下,你想如果是一级指针,test.c传进来的地址,再怎么操作是不会影响外面的,这里我们是需要重新定义外面的头节点的

下面的代码就会判断当这头节点为空那么我们创建一个新节点,并把新节点的地址传给pphead,那么这个pphead就是一个崭新的地址了

一起Debug看一下

首先phead传进listListPushBack函数时值为NULL,pphead的指针和newnode新创建的地址是不一样的

这个函数跑完之后,可以看到原先传进来的pphead地址0x77ffeefbff548到外面变成了0x10180f340

下面是push a new node at the end的函数实现

void linkListPushBack(LST** pphead, DataType x){
    
    if(*pphead == NULL){
        LST* newnode = createLSTNode(x);
        *pphead = newnode;
    }else{
		LST* cur = *pphead;
	    while(cur->next){
	        cur = cur->next;
	    }
	    LST* newnode = createLSTNode(x);
	    cur->next = newnode;
    }

}

4. linkListPrint

打印所有节点信息

void linkListPrint(LST* phead){
    assert(phead);
    
    LST* cur = phead;
    while(cur){
        printf("%d->", cur->data);
        cur = cur->next;
    }
    printf("NULL\\n");
}

5. linkListDestory

⚠️ 注意这里不能直接free掉头节点,如果这样后面都变成了随机值,所以先保存当前Node的下一个节点,然后把当前节点的去掉,以此循环,最终把头置空

void linkListDestory(LST** pphead){
    assert(pphead); //头指针必须有地址,不能为空
    //free(phead);     
    LST* cur = *pphead;
    while(cur){
        LST* next = cur->next;
        free(cur);
        cur = next;
    }
    *pphead = NULL;
}

6. linkListSize

计算链表的长度

int linkListSize(LST* phead){
    assert(phead);
    int count = 0;
    LST* cur = phead;
    while(cur){
        cur= cur->next;
        count++;
    }
    return count;
}

7. linkListEmpty

检查当前节点是否为空,这里我们包含
#include <stdbool.h>

bool linkListEmpty(LST* phead){
    return phead==NULL;
}

8. linkListPushFront

进行头插,时间是O(1),相比数组快的不是一点点
头插实现只需在头节点前创建一个新Node将其链接即可,而数组需要向后挪动N-1个位置才能头插

void linkListPushFront(LST** pphead, DataType x){
    if(pphead==NULL){
        LST* newnode = createLSTNode(x);
        *pphead = newnode;
    }else{
        LST* newnode = createLSTNode(x);
        newnode->next = *pphead;
        *pphead = newnode;
    }
}

9. linkListPopBack

创建两个指针,一个用于存储最后一个节点cur,另一个存储最后节点的前一个节点pre,从头开始向后便利,找到最后一个节点后free掉,并让pre的下一个指向NULL

void linkListPopBack(LST** pphead){
    assert(*pphead);
    // 这种情况不行,如果仅有一个节点的话,cur->next->next会越界
    //    LST* cur = *pphead;
    //    while(cur->next->next){
    //        cur = cur->next;
    //    }
    //
    //    LST* next = cur->next;
    //    cur->next = NULL;
    //    free(next);
    
    // 考虑用两个指针
    // 判断只有一个节点的情况和一个以上的情况
    if( (*pphead)->next == NULL){
        free(*pphead);
        *pphead = NULL;
    }else{
        LST* pre = NULL;
        LST* cur = *pphead;
        while(cur->next){
            pre = cur;
            cur = cur->next;
        }
        if(cur->next == NULL){
            free(cur);
            pre->next = NULL;
        }
    }
}

10. linkListPopFront

pop头节点,时间复杂度也是O(1)
至此我们的增删都写完了,和数组相比头插和头删都有了很大的优化,但是尾插和尾删的时间复杂度仍然是O(N)不是很理想,下一篇就会介绍双链表,这个就更厉害了!无论是头插头删还是尾插尾删时间都是O(1)

void linkListPopFront(LST** pphead){
    assert(pphead);
    assert(*pphead != NULL );
    
    //判断只有一个节点,和两个节点的情况
    if((*pphead)->next == NULL){
        free(*pphead);
        *pphead = NULL;
    }else{
        LST* next = (*pphead)->next;
        free(*pphead);
        *pphead = next;
    }
}

11. linkListNodeFind

找到指定值并返回,如果找不到返回空即可

LST* linkListNodeFind(LST** pphead, DataType x){
    LST* cur = *pphead;
    while(cur){
        if(cur->data == x){
            return cur;
        }else{
            cur = cur->next;
        }
    }
    return NULL;
}

12. linkListInsert

在某个位置插入一个值,注意单链表不适合在前面插入,更适合在pos之后的位置插入

void linkListInsert(LST** pphead, LST* pos, DataType x){
    assert(pphead);
    
    //分三种情况,头插,尾插,和中间任意位置
    if(*pphead == pos){
        linkListPushFront(pphead, x);
    }else{
        //中间插入
        LST* pre = *pphead;
        while(pre->next != pos){
            pre = pre->next;
        }
        
        //创建新节点
        LST* newnode = createLSTNode(x);
        newnode->next = pos;
        pre->next = newnode;
    }
}

13. linkListErase

在某个位置删除一个值

void linkListErase(LST** pphead, LST* pos){
    assert(pphead);
    
    //1. 删头节点
    //2. 删头以后的节点
    if(pos == *pphead){
        linkListPopFront(pphead);
    }else{
        //找到pos位置的前一个节点
        LST* pre = *pphead;
        while(pre->next != pos){
            pre = pre->next;
        }
        
        //找到后
        pre->next = pos->next; //pre的下一个指向当前要删节点(pos)的下一个
        free(pos);
        pos = NULL;
    }
}

四、完整代码

对于测试代码建议写完前面几个函数后,最好先测一下,不然会增下后续debug的时间成本

//
//  test.c
//  LinkList
//
//  Created by Henry on 2021/8/18.
//  Copyright © 2021 Henry. All rights reserved.
//

#include "linkList.h"

void TestFunc1(){
    
    LST* phead=NULL;
    
    linkListPushBack(&phead, 1);
    linkListPushBack(&phead, 2);
    linkListPushBack(&phead, 3);
    linkListPushBack(&phead, 4);
    linkListPushBack(&phead, 5);
    
    linkListPushFront(&phead, 100);
    linkListPrint(phead);
    
    linkListPopBack(&phead);
    linkListPrint(phead);
    
    linkListPopBack(&phead);
    linkListPrint(phead);
    
    linkListPopFront(&phead);
    linkListPrint(phead);
   
    LST* find = linkListNodeFind(&phead, 2);
    if(find){
        linkListInsert(&phead, find, 200);
    }else{
        printf("cannot find it \\n");
    }
    linkListPrint(phead);
    
    linkListInsert(&phead, find, 300);
    linkListPrint(phead);
    
    LST* find2 = linkListNodeFind(&phead, 1);
    linkListErase(&phead, find2);
    linkListPrint(phead);
}

int main(int argc, const char * argv[]) {
    TestFunc1();
    return 0;
}

Gitee链接🔗 🔗 🔗

👉👉👉 LinkList完整代码 👈👈👈👈

创作不易,如果文章对你帮助的话,点赞三连哦:)

以上是关于链表(LinkList)数据结构的基本操作实现详解,纯干货的主要内容,如果未能解决你的问题,请参考以下文章

LinkList(双向链表实现)

链表的基本操作java语言实现

数据结构之链表篇(单链表的常见操作)

链式链表的C风格实现

ArrayList、linklist、list的区别

算法2---链表4---单循环链表