用数组模拟链表

2,221 阅读5分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

零 前言

为什么放着现成的不用,要用数组来模拟链表?

原因是:这样相当于自己做了一个内存池,可以避免内存泄漏而且方便调试。更深一点来说,数组的存储位置集中,有利于提高Cache命中率。

当然,最重要的是效率原因。算法题中的数据大多十万到百万级别,如果用 new 的方法,很容易TL也就是超时。

所以掌握用数组模拟链表的方法很重要,本篇主要讲述单链表和双链表的模拟。

提示:本文为C++实现,但所有语言通用,会省略部分与实现无关的代码。会跳过一些基础概念,不了解百度即可,咱们直接看实现 ❤

一 单链表

image-20210823231503911

实现

我们使用 ene 数组分别存储数据和指针,同时初始化 headidx

const int N = 100010// 定义常量

// e[i]表示结点i的值
// ne[i]表示结点i的next指针的指向
// head表示头结点的下标,初始为-1
// idx存储当前用到了哪个结点,也可看作指针
int e[N], ne[N],head = -1, idx;	

img

然后就是实现基础操作插入和删除

// 头插法:将x插到头结点
void insert_head(int x) {
    e[idx] = x;
    ne[idx] = head;	// 1.将x项指向原先的头结点
    head = idx;		// 2.x项为新的头结点
    idx++;
}

// 普通插入(与头插类似):在第k个插入的数后面插入x项,k从0开始(下同)
// 先将x项指向k项的后一项,再让k项指向x项
void insert(int k, int x) {
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}

// 删除第k个插入的数
void remove(int k) {
    ne[k] = ne[ne[k]];	// 直接将该项指针指向后一项的后一项
}

img

可能大家都发现了,删除结点后,我们用过的数组项不会再使用。这样的好处就是我们不需要再花时间管理内存,只要数组开大一点就可以了,速度也会更快,毕竟数组是O(1)的随机存取。同时 NULL 用-1代替,不需在意空指针异常。

// 遍历
for (int i = head; i != -1; i = ne[i]) {
     cout << e[i] << ' ';
}

应用

  1. 多个单链表(邻接表)常用于树和图的存储,树是一种特殊的图,与图的存储方式相同。

    对于无向图的边ab,我们存储两条有向边 a->b,b->a,所以我们只考虑有向图的存储:

    // 对于每个点k,都建一个单链表存储所有可以走到的点。
    // h[k]存储每个单链表的头结点
    int h[N], e[N], ne[N], idx;
    
    // 初始化
    memset(h, -1, sizeof h);
    
    // 添加一条边 a->b
    void add(int a, int b) {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
  2. 哈希表的拉链法

    // h[k]存储每个单链表的头结点
    int h[N], e[N], ne[N], idx;
    memset(h, -1, sizeof h);
    
    // 将x插入哈希表
    void insert(int x) {
        int k = (x % N + N) % N;
        // 头插
        e[idx] = x, ne[idx] = h[k], h[k] = idx++;
    } 
    
    // 查找x是否在哈希表内
    int find(int x) {
        int k = (x % N + N) % N;
        // 遍历查找
        for (int i = h[k]; i != -1; i = ne[i])
            if (e[i] == x)
                return true;
        return false;
    }
    

二 双链表

image-20210823231542387

实现

lr 数组分别存储左右指针,将0作为左端点,1作为右端点idx 从2开始:

const int N = 100010;
int e[N], l[N], r[N], idx;

// 初始化
void init() {
    r[0] = 1, l[1] = 0;
    idx = 2;
}

与单链表插入删除相似,我们只需要注意两个指针的指向顺序即可:

img

// 在第k个插入的数右边插入x项
void insert(int k, int x) {
    e[idx] = x;
    l[idx] = k, r[idx] = r[k];	// 1.将x项左右指针分别指向第k项和第k项指向的结点
    l[r[k]] = idx;		// 2.k项指向的结点的左指针指回x项
    r[k] = idx;			// 3.k项右指针指向x项
    idx++;
    
    // 可如单链表一样用逗号省略为一句
}

// 删除第k项
void remove(int k) {
    l[r[k]] = l[k];
    r[l[k]] = r[k];		// 两句可以调换顺序,不会断链
}

与单链表不同,我们实现一个插入即可实现头插、尾插、左插以及右插

// 在链表的最左端插入数x
insert(0, x);
// 在链表的最右端插入数x
insert(l[1], x);
// 第k个插入的数左侧插入数x
insert(l[k], x);
// 第k个插入的数右侧插入数x
insert(k, x);
    
// 遍历, 注意i应从r[0]开始
for (int i = r[0]; i != 1; i = r[i]) {
    cout << e[i] << ' ';
}

三 总结

本篇思路主要来自AcWing,看完后可以去找模板题实践一下。

掌握了数组模拟链表的方法,动态链表即用类实现调用函数自然也能轻松掌握。

链表最明显的好处就是允许插入和删除表上任意位置上的节点,坏处就是不允许随机存取。

其实链表就和一般数组一样,只是他们的索引值形式不同,一般数组是有序的索引,而链表是地址值来连接的。所以我们可以将他视为一个数组,只是索引值处理不同


都看到这里了,点个赞呗(疯狂暗示)😆

有任何问题都可以评论留言~ 可以互相关注交流

更多有趣文章:Mancuoj 的个人主页