链表(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)数据结构的基本操作实现详解,纯干货的主要内容,如果未能解决你的问题,请参考以下文章