数据结构——链表

链表也是一种常见的数据结构,从数据结构类型上区分,链表属于存储结构的一种:链式存储结构。
和顺序存储结构一样,链式存储结构也可以用来作为实现其他数据结构的基础,比如前面介绍的线性表、栈、队列等。

顺序存储结构和链式存储结构的区别

使用顺序存储结构存放数据时,使用的是内存空间中连续的内存,如下图所示:


连续内存空间

使用顺序存储结构时,数据需要存放在连续的内存空间中。这种存储结构的优点在于读取速度很快,缺点在于无法有效利用内存空间。怎么理解呢?举个例子,如果只使用顺序存储结构,在程序的运行过程中,会产生很多的临时变量,这些变量在不用时会被垃圾回收,当变量被回收后,内存中就出现了不连续的情况,也可以叫做内存空洞。如下图所示,黑色区域表示内存空洞:


内存空洞

随着程序的运行,这样的内存空洞会越来越多,此时如果程序中需要用到较大的内存空间,而剩余的连续内存不够的情况下,将无法成功分配内存,但内存中又有很多的内存空洞,却无法得到有效利用。
要解决这个问题,有两个解决方案:
  1. 将内存空洞后面的数据往前移
    第一种方式就是每隔一段时间,将内存空洞后面的数据向前移,以达到“挤压”内存空洞的目的。这种方式的缺点在于性能浪费严重。
  2. 使用链式存储结构
    第二种方式就是使用链式存储结构:在某些内存空洞中存放一部分数据,再存放下一块可用内存空洞的地址,通过地址把这些可用的内存空洞连接起来,实现一个链式的结构,这就是链表。下面是链表的基本结构:


    链表

    上图展示了链表的基本结构,对于链表,其中的每个元素我们称之为节点。可见每个节点至少需要包含两部分内容:

  • 数据区:用来存放数据
  • 地址区:用来存放下一个节点的地址

链表的分类

通常我们说的链表也叫作单向链表,此外,链表还有两个常用的衍生结构:双向链表和循环链表。本文主要介绍单向链表的实现,双向链表和循环链表在后文介绍。

链表的代码实现

下面是链表的代码实现,首先来定义一个 ISingleNode 接口和 SingleNode 类,用来实现链表中的节点。

节点类和接口的代码实现

interface ISingleNode<T>{
    // 数据区
    data:T;
    // 地址区
    next:ISingleNode<T>;
}

class SingleNode<T> implements ISingleNode<T>{
    data:T = null;
    next:ISingleNode<T> = null;
    constructor(data?:T){
        this.data = data;
    }
}

链表类的接口和代码实现

首先定义 ISingleLinkedList 接口,规范链表类中的基本操作方法:

interface ISingleLinkedList<T>{
    // 获取链表的长度
    size():number;
    // 获取链表头
    head():ISingleNode<T>;
    // 增加节点
    append(item:T):ISingleNode<T>;
    // 删除节点
    remove(item:T):void;
    // 根据位置删除
    removeAt(pos:number):void;
    // 插入节点
    insert(newItem:T,oldItem:T):ISingleNode<T>;
    // 在具体的位置插入
    insertAt(newItem:T,pos:number):ISingleNode<T>;
    // 清空链表
    clear():void;
    // 判断链表是否为空
    isEmpty():boolean;
    // 查找节点和其前驱
    find(item:T):{
        previous:ISingleNode<T>,
        current:ISingleNode<T>,
        currentPos:number,
        previousPos:number
    };
    // 根据位置查找节点和其前驱
    findAt(pos:number):{
        previous:ISingleNode<T>,
        current:ISingleNode<T>,
        currentPos:number,
        previousPos:number
    };
    // 获取链表中的元素
    toString():ISingleNode<T>[];
}

定义链表类 SingleLinkedList

class SingleLinkedList<T> implements ISingleLinkedList<T>{
    private _size:number = 0;
    private _head:ISingleNode<T> = new SingleNode();
}

下面逐个讲解链表类中的几个关键方法。

  1. 实现 find() 方法
    首先需要实现 find() 方法,该方法用来从链表中查找节点,以供后面的添加和删除方法使用。
find(item:T):{
    previous:ISingleNode<T>,
    current:ISingleNode<T>,
    currentPos:number,
    previousPos:number,
}{
    if(!item){
        throw new Error("参数错误!")
    }
    let 
        previous:ISingleNode<T> = null,
        current:ISingleNode<T> = this._head,
        index:number = -1;
    while(current){
        // 更新索引值
        index++;
        // 判断当前节点中的数据和传入的是否匹配
        if(current.data === item){
            break;
        }
        // 将 current 赋值给 previous
        // 将 current.next 赋值给 current
        // 在下一次迭代中使用
        previous = current;
        current = current.next;
    }

    // HACK 在前面的循环中找不到对于的元素时,会获取到尾节点
    // 这里进行一次二次验证
    if(current.data !== item){
        index = -1;
    }

    // 处理未找到的情况
    if(index === -1){
        return{
            previous:null,
            current:null,
            previousPos:-1,
            currentPos:-1
        }
    }
    return{
        previous,
        current,
        currentPos:index,
        // 前驱的位置在当前节点之前
        previousPos:index - 1
    }
}

这个方法(和配套的 findAt())是链表类中核心方法,再进行链表的添加、插入或删除操作时,首先需要查找链表中的某个节点和其前驱节点,查找总是从链表的头部开始的。

  1. 实现 append() 方法
    该方法用来向链表中添加节点,在添加时有两种情况:
  • 链表为空时,将头节点替换为新节点
  • 链表不为空时,将尾结点替换为新节点
append(item:T):ISingleNode<T>{
    const newNode = new SingleNode<T>(item);
    // 链表中没有节点
    if(!this._size){
        this._head = newNode;
    }else{
        const {current} = this.findAt(this._size - 1);
        current.next = newNode
    }
    this._size++;
    return newNode;
}

3.实现 insert() 方法
该方法用来向链表中插入元素,也有两种情况:

  • 插入的位置是头节点,将头节点替换为新节点
  • 插入的位置是其他节点,将原始节点的位置插入新节点,同时将原始节点向后移,这时就需要用到原始节点的前驱节点
insert(newItem:T,oldItem:T):ISingleNode<T>{
    // 创建新节点
    const newNode = new SingleNode(newItem);
    // 查找旧节点及其前驱节点
    const {current,previous} = this.find(oldItem);
    // 没有查找到旧节点,直接返回
    if(!current) return null;
    // 当 previous 为 null 时,说明是头节点
    if(!previous){
        newNode.next = current;
        this._head = newNode;
    }else{
        // 将新建节点的 next 指向旧节点
        newNode.next = current;
        // 将旧节点前驱的 next 指向新建的节点
        previous.next = newNode;
    }
    this._size++;
    return newNode;
}
  1. 实现 remove() 方法
    该方法用来从链表中移除节点。
remove(item:T):void{
    // 获取当前节点和其的前驱
    let { current,previous } = this.find(item);
    // 还没有添加节点的情况
    if(!current) return;
    // 没有前驱节点,说明是头节点
    if(!previous){
        this._head = current.next;
    }else{
        // 将当前节点的前驱的 next 指向当前节点的后继
        previous.next = current.next;
    }
    // 移除当前节点
    current = null;
    // 更新链表长度
    this._size--;
}

下面是 SingleLinkedList 类的完整代码实现:

class SingleLinkedList<T> implements ISingleLinkedList<T>{
    private _size:number = 0;
    private _head:ISingleNode<T> = new SingleNode();
    size():number{
        return this._size;
    }
    head():ISingleNode<T>{
        return this._head;
    }
    clear():void{
        this._head = null;
        this._head = new SingleNode();
        this._size = 0;
    }
    isEmpty():boolean{
        return !this._size;
    }
    append(item:T):ISingleNode<T>{
        const newNode = new SingleNode<T>(item);
        // 链表中没有节点
        if(!this._size){
            this._head = newNode;
        }else{
            const {current} = this.findAt(this._size - 1);
            current.next = newNode
        }
        this._size++;
        return newNode;
    }
    find(item:T):{
        previous:ISingleNode<T>,
        current:ISingleNode<T>,
        currentPos:number,
        previousPos:number,
    }{
        if(!item){
            throw new Error("参数错误!")
        }
        let 
            previous:ISingleNode<T> = null,
            current:ISingleNode<T> = this._head,
            index:number = -1;
        while(current){
            // 更新索引值
            index++;
            // 判断当前节点中的数据和传入的是否匹配
            if(current.data === item){
                break;
            }
            // 将 current 赋值给 previous
            // 将 current.next 赋值给 current
            // 在下一次迭代中使用
            previous = current;
            current = current.next;
        }

        // HACK 在前面的循环中找不到对于的元素时,会获取到尾节点
        // 这里进行一次二次验证
        if(current.data !== item){
            index = -1;
        }

        // 处理未找到的情况
        if(index === -1){
            return{
                previous:null,
                current:null,
                previousPos:-1,
                currentPos:-1
            }
        }
        return{
            previous,
            current,
            currentPos:index,
            // 前驱的位置在当前节点之前
            previousPos:index - 1
        }
    }
    findAt(pos:number):{
        previous:ISingleNode<T>,
        current:ISingleNode<T>,
        currentPos:number,
        previousPos:number,
    }{
        let 
            previous:ISingleNode<T> = null,
            current:ISingleNode<T> = this._head,
            index:number = -1;
            
        if(pos < 0 || pos > this._size - 1){
            throw  new Error("索引越界!");
        }

        while(current){
            index++;
            if(index === pos){
                break;
            }
            previous = current;
            current = current.next;
        }

        // 处理未找到的情况
        if(index === -1){
            return{
                previous:null,
                current:null,
                previousPos:-1,
                currentPos:-1
            }
        }
        return{
            previous,
            current,
            currentPos:index,
            // 前驱的位置在当前节点之前
            previousPos:index - 1
        }
    }
    remove(item:T):void{
        // 获取当前节点和其的前驱
        let { current,previous } = this.find(item);
        // 还没有添加节点的情况
        if(!current) return;
        // 没有前驱节点,说明是头节点
        if(!previous){
            this._head = current.next;
        }else{
            // 将当前节点的前驱的 next 指向当前节点的后继
            previous.next = current.next;
        }
        // 移除当前节点
        current = null;
        // 更新链表长度
        this._size--;
    }    
    removeAt(pos:number):void{
        // 获取当前节点和其的前驱
        let { current,previous } = this.findAt(pos);
        // 还没有添加节点的情况
        if(!current) return;
        // 没有前驱节点,说明是头节点
        if(!previous){
            this._head = current.next;
        }else{
            // 将当前节点的前驱的 next 指向当前节点的后继
            previous.next = current.next;
        }
        // 移除当前节点
        current = null;
        // 更新链表长度
        this._size--;
    }
    insert(newItem:T,oldItem:T):ISingleNode<T>{
        // 创建新节点
        const newNode = new SingleNode(newItem);
        // 查找旧节点及其前驱节点
        const {current,previous} = this.find(oldItem);
        // 没有查找到旧节点,直接返回
        if(!current) return null;
        // 当 previous 为 null 时,说明是头节点
        if(!previous){
            newNode.next = current;
            this._head = newNode;
        }else{
            // 将新建节点的 next 指向旧节点
            newNode.next = current;
            // 将旧节点前驱的 next 指向新建的节点
            previous.next = newNode;
        }
        this._size++;
        return newNode;
    }
    insertAt(newItem:T,pos:number):ISingleNode<T>{
        // 创建新节点
        const newNode = new SingleNode(newItem);
        // 查找旧节点及其前驱节点
        const {current,previous} = this.findAt(pos);
        if(!current) return null;
        // 当 previous 为 null 时,说明是头节点
        if(!previous){
            newNode.next = current;
            this._head = newNode;
        }else{
            // 将新建节点的 next 指向旧节点
            newNode.next = current;
            // 将旧节点前驱的 next 指向新建的节点
            previous.next = newNode;
        }
        this._size++;
        return newNode;
    }
    toString():ISingleNode<T>[]{
        const tmp:ISingleNode<T>[] = [];
        let current:ISingleNode<T> = this._head;
        while(current){
            tmp.push(current);
            current = current.next;
        }
        return tmp;
    }
}

总结

链表在实现上比前面几个数据结构要复杂一点,这是因为在实现链表的添加、插入和删除操作时还需要修复节点地址区的指向问题。通常,链表操作需要考虑两种情况:

  • 在头节点进行添加、插入和删除操作
  • 在其他位置进行添加、插入和删除操作

当在头节点进行操作时,不需要使用头节点的前驱节点,因为链表的头节点没有前驱节点,只需要修正头节点的 next 属性,或者使用新的节点替换头节点。
在其他位置进行操作时,需要使用到该节点的前驱节点,并修正前驱节点的 next 属性。同时,如果是删除操作,还需要使用到该节点的后继节点,在修正前驱节点和后继节点的地址区指向后,还需要将被删除的节点置为 null,防止内存泄露。

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容