数据结构之链表结构

  链表和数组一样,都是用于储存一系列的元素(数据)的数据结构,但是链表和数组的实现机制完全不同,下面我们就来学习一下另外一种非常常见的用于储存数据的线性结构:链表,

  要储存多个元素,数组(列表)可能是做常用的数据结构,我们之前说过,几乎每一种编程语言都会默认实现数组结构,尽管数组的操作很灵活通过下标访问元素效率很高,但是数组也有很多缺点比如数组的创建通常是需要申请一段连续的内存空间(一整块内存)并且大小是固定的(大多数编程语言都是固定的)所以如果当前数组不能满足容量需求的时候,则需要进行扩容操作(通常操作是审申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)而且数组开头或者中间位置插入数据的成本很高,需要进行大量元素的位移,尽管我们已经学习过js的array类方法可以帮我们做这些事,但是背后的原理依然是这样的,

  知道数组的优缺点之后那接下来就该轮到我们链表出场了,要储存一组元素另一个选择就是链表,但是不同于数组,链表中的元素在内存中不必是连续的空间,链表的每个元素由元素本身和一个指向下一个元素的引用(有些语言称为指针或连接)组成,可能你现在还不太理解,没关系下面我们会有相关的实现
  相对于数组,链表有一些优点,内存空间不是必须连续的,这样就可以充分利用计算机的内存,实现灵活的内存动态管理,链表不必在创建的时候就确定大小,并且大小可以无限延申下去,链表在插入和删除数据的时候,时间复杂度可以达到O(1),相对于数组效率要高很多。但是链表不支持通过下标去访问元素,只能通过链表中每个元素中的指针来遍历访问,这一点倒是不如数组,链表中访问任何一个位置的元素时,都需要从头开始访问,(无法跳过第一个元素访问任何一个元素)

  什么是链表呢,其实上面我们已经简单的提过了链表的结构,这里我们更加详细的进行分析,链表类似于火车:有一个火车头,火车头会链接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推

火车
链表结构

链表结构的实现

js中原生没有提供链表结构,下面我们就来自己实现一下链表结构,内部使用对象最为载体

// 简单的链表结构实现
class LinkedList {
  // 构造方法
  constructor() {
    // 链表头部指针
    this.head = null;
    // 创建新节点(内部类,仅供内部方法使用)
    this._createNode = class {
      constructor(data) {
        this.data = data;
        this.next = null;
      }
    };
  }
  // get 内部使用遍历来获取链表长度,并返回
  get length() {
    let current = this.head;
    // 如果header为null则退出返回0
    if (!current) return 0;
    let index = 0;
    while (current.next) {
      current = current.next;
      index++;
    }
    return index;
  }
  // 向链表尾部添加,接收一个参数:被添加的元素
  append(element) {
    // 创建新节点
    let newNode = new this._createNode(element);
    if (!this.head) {
      // 如果添加是是第一个节点,直接将 head 指针指向此节点
      this.head = newNode;
    } else {
      // 如果添加的不是第一个节点
      let current = this.head;
      // 依次循环直到,到达链表末尾
      while (current.next) {
        // 首次进入循环的时候,next方法是第一个元素的next (因为head指向的就是第一个元素)
        // 然后将 current 重新赋值,指向下一个的 next,然后重新进入循环,直到 next为 null
        current = current.next;
      }
      // 退出循环后
      // 将最后一个元素的next 指向新节点
      current.next = newNode;
    }
  }
  // 在链表指定位置添加元素,,接受两个参数,第一个是位置,第二个是要添加的元素
  insert(position, element) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    // 创建元素
    let newNode = new this._createNode(element);
    // 如果是0则直接向头部添加
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else if (position === this.length) {
      // 如果要插入的位置正好是 链表的末尾就直接调用底部插入方法
      return this.append(element);
    } else {
      // 获取头部
      let current = this.head;
      // 遍历到指定的位置
      for (let i = 0; i <= position - 2; i++) {
        current = current.next;
      }
      // 新节点的 next 指向现在的下一个 元素
      newNode.next = current.next;
      // 中断原先结构,替换为新节点
      current.next = newNode;
    }
  }
  // 获取对应位置的元素
  get(position) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    // 获取头部
    let current = this.head;
    // 遍历到指定位置
    for (let i = 0; i <= position - 1; i++) {
      current = current.next;
    }
    return current;
  }
  // 返回元素在链表中的位置,如果没有返回 -1
  // 接受一个参数
  indexOf(element) {
    // 获取头部
    let current = this.head;
    // 预先设定 下标值
    let index = 0;
    // 进入循环
    while (current.next) {
      // 循环中判断
      if (element === current.data) {
        // 如果是则直接return 退出函数
        return index;
      }
      // 如果不是则重新赋值
      current = current.next;
      // 下标+1
      index += 1;
    }
    // 没有找到返回 -1
    return -1;
  }
  // 更新某个位置的元素
  update(position, element) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    // 获取头部
    let current = this.head;
    // 遍历到指定位置
    for (let i = 0; i <= position - 1; i++) {
      current = current.next;
    }
    // 直接更新
    current.data = element;
  }
  // 从链表的特定位置移除一项
  removeAt(position) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    // 获取头部
    let current = this.head;
    // 如果是0
    if (position === 0) {
      // 直接移除第一项
      this.head = this.head.next;
    } else {
      // 遍历到指定位置
      for (let i = 0; i <= position - 2; i++) {
        current = current.next;
      }
      // 移除
      current.next = current.next.next;
    }
  }
  // 从链表中移除指定项
  remove(element) {
    return this.removeAt(this.indexOf(element));
  }
  // 链表是否为空
  isEmpty() {
    // 直接判断header
    if (this.head) {
      return false;
    } else {
      return true;
    }
  }
  // 查看链表元素个数
  size() {
    // 直接返回 链表长度
    return this.length;
  }
  // 输出字符串
  toString() {
    // 获取头部
    let current = this.head;
    // 预定义字符串
    let listString = "";
    // 循环 链表
    while (current) {
      listString += " => " + current.data;
      current = current.next;
    }
    // 返回字符串
    return listString.slice(4);
  }
}

let cs = new LinkedList();

cs.append("小红");
cs.append("小绿");
cs.append("小黑");
// console.log(cs.length)
// console.log(cs.toString())
// cs.insert(1,"成功")
// console.log(cs.toString())
// cs.update(4,"成功更新")
console.log(cs.toString());

// console.log(cs.get(4))
cs.remove("小绿");
console.log(cs.toString());

通过上面的代码我们就简单的实现了一个链表结构

双向链表

  之前我们实现的链表其实可以称之为单向链表,另外还有一种常见的链表结构我们称之为双向链表,那什么是双向链表呢它和单向链表有什么不同呢?下面我们就来认识一下双向链表

  首先回顾一下单项链表,单向链表只能从头遍历到尾,也就是链表相连的过程是单向的,实现原理是上一个链表节点中有一个指向下一个链表节点的引用,也就是每个节点只包含一个向下的引用,这个特性就会带来一个明显的问题,我们可以轻松的到达下一个节点,但是如果想要回到上一个节点是很困难的,而在实际开发中,经常会遇到需要回到上一个节点的情况,举个例子:假设一个文本编辑器使用链表结构来储存内容,每一行用一个String对象存储在链表中的一个节点上,当编辑器用户向下移动光标的时候链表直接操作到下一个节点即可,但是当用户将光标向上移动呢?这个时候就需要回到上一个节点,如果是单向链表我们可能需要重新从first开始,依次遍历到想要的节点上

  下面是一张双向链表的结构示意图,通过此图我们就可以清楚的看到双向链表的结构以及它和单向链表的区别,之前单向链表中每个几点都只包含两个东西:数据和向下的指针,但是在双向链表的每个节点中多出了一样东西即向上的指针,也就是说我们可以通过此指针来访问上一个节点,这样就解决了上述问题,每个节点既可以访问下一个节点也可以访问上一个节点

双向链表结构

总结双向链表的特点:双向链表可以使用一个head和一个tail分别指向头部和尾部的节点,每个节点都由三部分组成:前一个节点的指针(prev),保存的元素(item),后一个节点的指针(next)。双向链表的第一个节点的prev是null,双向链表的最后一个节点的next是null

  那我们就来具体看一下双向链表具体如何实现

// 双向链表结构实现
class DoublyLinkedList {
  // 构造方法
  constructor() {
    // 链表头部指针
    this.head = null;
    // 链表尾部指针
    this.tail = null;
    // 创建新节点(内部类,仅供内部方法使用)
    this._createNode = class {
      constructor(data) {
        this.data = data;
        // 指针默认为 null
        this.prev = null;
        this.next = null;
      }
    };
  }
  // get 内部使用遍历来获取链表长度,并返回
  get length() {
    let current = this.head;
    if (!current) return 0;
    let index = 0;
    while (current.next) {
      current = current.next;
      index++;
    }
    return index;
  }
  // 向链表尾部添加,接收一个参数,被添加的元素
  append(element) {
    // 创建新节点
    let newNode = new this._createNode(element);
    // 如果当前链表为空
    if (!this.head && !this.tail) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      // 如果添加的不是第一个节点
      let current = this.tail;
      current.next = newNode;
      newNode.prev = current;
      this.tail = newNode;
    }
  }
  // 向链表特定位置添加元素
  insert(position, element) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    // 创建元素
    let newNode = new this._createNode(element);
    // 0 代表头部
    if (position === 0) {
      newNode.next = this.head;
      this.head = newNode;
    } else if (position === this.length) {
    // 如果要插入的位置正好是 链表的末尾就直接调用底部插入方法
      return this.append(element)
    } else {
      //  中间插入
      // 获取头部
      let current = this.head;
      for (let i = 0; i <= position - 2; i++) {
        current = current.next;
      }
      // 新节点的 next 指向现在的下一个 元素
      newNode.next = current.next;
      // 中断原先结构,替换为新节点
      current.next = newNode;
    }
  }
  // 获取对应位置的元素
  get(position) {
    // 越界判断
    if (position < 0 || position > this.length) return false;
    let current = this.head;
    for (let i = 0; i <= position - 1; i++) {
      current = current.next;
    }
    return current;
  }
  // 返回元素在链表中的位置,如果没有返回 -1
  indexOf(element) {
    let current = this.head;
    let index = 0;
    while (current.next) {
      if (element === current.data) {
        return index;
      }
      current = current.next;
      index += 1;
    }
    return -1;
  }
  // 更新某个位置的元素
  update(position, element) {
    if (position < 0 || position > this.length) return false;
    let current = this.head;
    for (let i = 0; i <= position - 1; i++) {
      current = current.next;
    }
    current.data = element;
  }
  // 从链表的特定位置移除一项
  removeAt(position) {
    if (position < 0 || position > this.length) return false;
    let current = this.head;
    if (position === 0) {
      this.head = this.head.next;
    } else {
      for (let i = 0; i <= position - 2; i++) {
        current = current.next;
      }
      current.next = current.next.next;
    }
  }
  // 从链表中移除指定项
  remove(element) {
    return this.removeAt(this.indexOf(element));
  }
  // 链表是否为空
  isEmpty() {
    if (this.head && this.tail) {
      return false;
    } else {
      return true;
    }
  }
  // 查看链表元素个数
  size() {
    return this.length;
  }
  // 输出字符串
  toString() {
    let current = this.head;
    let listString = "";
    while (current) {
      listString += " => " + current.data;
      current = current.next;
    }
    return listString.slice(4);
  }
}

let cs = new DoublyLinkedList();

cs.append("第一")
cs.append("第二")
cs.append("第三")
cs.append("第四")
cs.append("第五")
cs.insert(4,"插入1")

console.log(cs.toString())


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

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,654评论 0 13
  • 基本概念 链表的含义: 链表是一种用于存储数据集合的数据结构,具有以下属性 相邻元素之间通过指针相连 最后一个元素...
    古剑诛仙阅读 1,004评论 0 3
  • 链表:数据存储结构我们通过一个简单的场景,了解一下链表的数据存储结构。那就是LRU缓存淘汰算法。 缓存是一种提高数...
    初心myp阅读 618评论 0 1
  • 顺序表结构的存储方式非常容易理解,操作也十分方便,但是顺序结构有如下缺点: 1.在插入或删除时,往往需要移动大量数...
    雨飞飞雨阅读 536评论 0 2
  • 一、链表的定义 链表是一种递归的数据结构,是一种线性结构,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下...
    熊喵先森阅读 1,464评论 0 3