链表

概述

链表是物理存储单元上非连续的、非顺序的存储结构,由一系列节点组成。

节点

节点包含两部分,一部分是存储数据元素的数据域,一部分是存储指向下一个节点的指针域。定义一个节点可以用如下代码:

function Node (data) {
    this.data = data;
    this.next = null;
}

链表中的第一个节点是首节点,最后一个节点是尾节点。

有头链表和无头链表

无头链表是指第一个节点既有数据域,又有指针域,第一个节点即是首节点又是头节点。有头链表是指第一个节点只有指针域,没有数据域。

有头链表.png

有头链表浪费空间,但是对于首个元素的操作(插入、删除等)可以跟其他节点相同,边界好处理。无头链表节省空间,但是处理时要注意边界情况。这里使用的是无头链表。

定义链表类:
function LinkList() {
    // 定义节点
    function Node (data) {
        this.data = data;
        this.next = null;
    }
    
    let length = 0; // 长度
    let head = null; // 头节点
    let tail = null; // 尾节点
}

给链表添加一些方法:

  • append:添加一个新元素
  • insert:在指定位置插入一个元素
  • remove: 删除指定位置的元素
  • indexOf: 返回指定元素的索引
  • get: 返回指定索引位置的元素
  • isEmpty: 判断链表是否为空
  • print: 打印整个链表
function LinkList () {
  // 定义节点
  function Node (data) {
      this.data = data;
      this.next = null;
  }
  
  let length = 0; // 长度
  let head = null; // 头节点
  let tail = null; // 尾节点

  this.append = function (data) { // 添加一个新元素
    const node = new Node(data); // 创建一个新节点
    if (!head) { // 如果是空链表,头节点和尾节点都指向新节点
      head = node;
      tail = node;
    } else {
      tail.next = node; // 尾节点指向新创建的节点
      tail = node; // tail指向最后一个节点
    }
    length++;

    return true;
  }

  this.insert = function (index, data) { // 在指定位置插入一个元素
    if (index < 0 || index > length) { // 范围错误
      return false;
    }
    const node = new Node(data);
    if (index === 0) { // 如果在头节点前面插入,新的节点就变成了头节点
      node.next = head;
      head = node;
    } else {
      let amount = 1;
      let currentNode = head;
      while (amount < index) {
        currentNode = currentNode.next;
        amount++;
      }
      node.next = currentNode.next;
      currentNode.next = node;
    }

    length++;
    return true;
  }

  this.remove = function (index) { // 删除指定位置的元素
    if (index < 0 || index >= length) {
      return false;
    }
    let delNode = null; // 记录要删除的节点
    if (index === 0) { // 如果删除头节点,head指向下一个节点
      delNode = head;
      head = head.next;
      if (!head) { // 如果此时head是null,说明之前只有一个节点,删除之后变为空链表
        tail = null; // 尾节点置空
      }
    } else {
      let amount = 0;
      let preNode = null;
      let currentNode = head;
      while(amount < index) {
        preNode = currentNode;
        currentNode = currentNode.next;
        amount++;
      }
      delNode = currentNode;
      preNode.next = currentNode.next;

      if (!currentNode.next) { // 如果删除的是尾节点,tail指向前一个节点
        tail = preNode;
      }
    }
    length--;
    tail.next = null;
    return tail.data;
  }

  this.indexOf = function (data) { // 返回指定元素的索引
    let currentNode = head;
    let index = -1;
    while (currentNode) {
      index++;
      if(currentNode.data === data) { // 有的话返回第一个
        return index;
      }
      currentNode = currentNode.next;
    }
    return -1; // 如果没有返回-1
  }

  this.get = function (index) { // 返回指定索引位置的元素
    if (index < 0 || index >= length) {
      return null;
    }
    let currentIndex = 0;
    let currentNode = head;
    while (currentIndex < index) {
      currentIndex++;
      currentNode = currentNode.next;
    }
    return currentNode.data;
  }

  this.isEmpty = function () { // 判断链表是否为空
    return length === 0;
  }

  this.print = function () { // 打印整个链表
    let currentNode = head;
    while (currentNode) {
      console.log(currentNode.data);
      currentNode = currentNode.next;
    }
  }
}

之前用数组实现了栈和队列,现在可以基于链表实现。

为了实现方便,再给链表添加几个方法:

this.head = function () { // 返回头节点的值
    return this.get(0);
}

this.tail = function () { // 返回尾节点的值
    return this.get(length - 1);
}

this.removeHead = function () { // 删除头节点
    return this.remove(0)
}

this.removeTail = function () { // 删除尾节点
    return this.remove(length - 1)
}

基于链表实现的栈:

function Stack() {
  let linkList = new LinkList();

  this.push = function (item) { // 入栈
    linkList.append(item);
  }

  this.pop = function () { // 出栈
    return linkList.removeTail();
  }

  this.top = function () { // 返回栈顶元素
    return linkList.tail()
  }
}

基于链表实现的队列:

function Queue() {
  let linkList = new LinkList();

  this.enqueue = function (item) { // 入队列
    linkList.append(item);
  }

  this.dequeue = function () { // 出队列
    return linkList.removeHead();
  }

  this.head = function () { // 返回队首元素
    return linkList.head();
  }

  this.tail = function () { // 返回队尾元素
    return linkList.tail();
  }
}

练习题

一、翻转链表

翻转之前:


link3.png

翻转之后:


link4.png

1、迭代翻转

假设有三个节点的一个链表,我们设preNode指向前一个节点, currentNode指向是当前要翻转的节点,在当前节点进行翻转:

currentNode.next = preNode;

在遍历过程中,每完成一个节点的翻转,都让currentNode = nextNode,指向下一个要翻转的节点,preNode和nextNode也一起向后滑动。

对于头节点来说,它翻转过后就是尾节点,尾节点的next指向null,所以初始化preNode = null;

link5.png
function reverseIter(head) {
  if (!head) { // 如果是空链表,直接返回
    return null;
  }
  let preNode = null; // 前一个节点
  let currentNode = head; // 当前要翻转的节点
  while(currentNode) {
    let nextNode = currentNode.next; // 记录下一个节点
    currentNode.next = preNode; // 翻转当前节点,让他指向前一个节点

    preNode = currentNode; // 后移
    currentNode = nextNode;
  }

  return preNode; // 返回翻转之后的头节点
}
  1. 递归翻转

看视频时听张老师说递归的精髓在于甩锅,你做不到的事情,让别人去做,等别人做完了,你在别人的基础上继续做。我觉得总结的很精辟。

  • 首先明确函数的功能,既然是先让别人去做,得清楚的告诉他做什么。当前函数的功能,就是从head开始翻转链表,并返回翻转后的头节点
  • 正式甩锅,进行递归调用
let newHead = reverseDigui(head.next)

原本是翻转以head开头的链表,可是不会啊,就交给下一个head.next去翻转。

  • 根据别人的结果,计算自己的结果

第二步中,已经完成了从head.next开始翻转链表,那么,新链表的尾节点就是head.next, 现在,只需要把head连接到新链表中,head.next.next = head, 新链表的尾节点就变成head了。

  • 找到递归终止条件

函数要返回新链表的头节点,新链表的头节点正是旧链表的尾节点,所以遇到尾节点时,递归就终止了

link6.png
function reserveDigui(head) {
  if (!head) {
    return null;
  }
  if (!head.next) { // 递归终止条件,返回值是新链表的头节点
    return head;
  }
  let newHead = reserveDigui(head.next);
  head.next.next = head;
  head.next = null;
  return newHead;
}
  1. 合并两个有序链表
    已知两个有序链表(元素从小到大),将两个链表合并成一个有序链表,并返回新链表,原有链表不要修改

思路分析:

  • 如果两个链表中其中一个是空链表,直接返回不为空的那个就行了。
  • 否则就设置一个循环,对当前的数值进行比较,小的那个放到新链表中,直到其中一个链表节点或者两个链表节点都是null时,结束循环
  • 如果还有链表没到达尾节点,循环遍历添加到新链表中。
function mergeLink(head1, head2) {
  if (!head1) {
    return head2;
  }
  if (!head2) {
    return head1;
  }
  let mergeHead = null; // 新链表头
  let mergeTail = null; // 新链表尾
  let curr1 = head1;
  let curr2 = head2;
  while(curr1 && curr2) {
    let minData; // 记录最小值
    if (curr1.data <curr2.data) {
      minData = curr1.data;
      curr1 = curr1.next;
    } else {
      minData = curr2.data;
      curr2 = curr2.next;
    }

    if (!mergeHead) { // 头节点指向
      mergeHead = new Node(minData);
      mergeTail = mergeHead;
    } else {
      let newNode = new Node(minData);
      mergeTail.next = newNode; // 添加新节点
      mergeTail = newNode // 尾节点后移
    }
  }

  let restLink = curr1 || curr2; // 还没到达尾节点的链表
  while(restLink) { // 循环加进新链表中
    let newNode = new Node(restLink.data);
    mergeTail.next = newNode;
    mergeTail = newNode;
    restLink = restLink.next;
  }

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

推荐阅读更多精彩内容

  • 搞懂单链表常见面试题 Hello 继上次的 搞懂基本排序算法,这个一星期,我总结了,我所学习和思考的单链表基础知识...
    醒着的码者阅读 4,591评论 1 45
  • 有头链表(注意:头结点有的地方是全局变量) 初学者学自于大话数据结构,不足及错误的地方请读者提出来,谢谢。 可加 ...
    lxr_阅读 800评论 0 2
  • 什么是数组? 数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下...
    启明_b56f阅读 914评论 0 0
  • 一,打野不忘救队友,因为人头比野值钱; 二,收完一路兵线别急着推塔,赶紧去人最多的地方团;当然如果我方已经在团而且...
    神经病人玩游戏阅读 469评论 1 7
  • 一、使用SharedPreferences存储数据 默认存储路径:/data/data/ /shared_pref...
    zoky_阅读 891评论 0 1