数据结构学习--链表

一、什么是链表?

链表是物理存储单元上非连续的、非顺序的存储结构,不一样于栈和队列。链表由一系列节点组成,每一个结点包括两个部分:一个是存储数据元素的数据域,另外一个是存储下一个结点地址的指针域。javascript

因为没必要须按顺序存储,链表在插入的时候能够达到O(1)的复杂度,比另外一种线性表顺序表快得多,可是查找一个节点或者访问特定编号的节点则须要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。使用链表结构能够克服数组链表须要预先知道数据大小的缺点,链表结构能够充分利用计算机内存空间,实现灵活的内存动态管理。可是链表失去了数组随机读取的优势,同时链表因为增长告终点的指针域,空间开销比较大。java

下面为链表的结构示意图 node

1.一、节点

节点包含了两部分,一部分是存储数据的元素区域,一部分是指向下一个节点的指针区域,上图中绿色部分表示数据区域,蓝色部分表示指针区域,它们共同构成一个节点。数组

定义一个节点:app

let Node = function(data) {
    this.data = data // 数据
    this.next = null // 指针
}
// 建立新的节点
let node1 = new Node(1);
let node2 = new Node(2);
let node3 = new Node(3);
node1.next = node2;
node2.next = node3
复制代码

1.二、首尾节点

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

1.三、有头链表和无头链表

  1. 无头链表是指第一个节点既有数据域,又有指针域,第一个节点既是首节点又是头节点。学习

  2. 有头链表是指第一个节点只有指针域,而没有数据域。一般有头链表的数据域能够存放当前的链表的一些信息。ui

在链表定义中展现的就是无头链表,一个有头链表的结构图以下:this

二、链表的实现

2.一、定义链表类

function LinkList() {
    let Node = function(data) {
        this.data = data
        this.next = null
    }
    let length = 0   // 链表长度 
    let head = null  // 头节点
    let tail = null  // 尾节点
}
复制代码

2.二、链表的方法

  • append, 添加一个新的元素
  • insert,在指定位置插入一个元素
  • remove,删除指定位置的节点
  • get,返回指定索引位置的元素
  • print,打印整个链表

2.2.一、append

  • 每次append,都要先建立一个node节点,若是列表为空,则让head和tail指向这个新建立的节点
  • 若是列表不为空,则tail.next = node, 并让tail指向node
this.append = (data) => {
    // 建立一个新的节点
    let new_node = new Node(data)
    
    // 判断是否为空链表
    if(head === null) {
        head = new_node
        tail = head
    } else {
        tail.next = new_node // 尾节点指向新建立的节点
        tail = new_node      // 让尾节点等于新建立的节点
    }
    length ++
}
复制代码

2.2.二、insert

append只能在链表的末尾添加元素,而insert能够在指定位置插入一个元素,新增数据的方式更加灵活,insert方法须要传入参数index,指明要插入的索引位置。该方法的关键是找到索引为index-1的节点,只有找到这个节点,才能将新的节点插入到链表中spa

this.insert = (index. data) => {
        if(index<0 || index > length) return // index无效值
        if(index === length) { // 当index等于length调用append方法
            return this.append(data)
        } else {
            let new_node = new Node(data)
            if(index === 0) { // 
                new_node.next = head
                head = new_node
            } else {
                let insert_index = 1
                let curr_node = head
                while(insert_index < index) {
                    insert_index ++
                    curr_node = curr_node.next
                }
                let next_node = curr_node.next // 记录当前节点下一个节点
                curr_node.next = new_node      // 当前节点下一个节点设为新节点
                new_node.next = next_node      // 看上面的图会更能明白
            }
        }
        length ++
}
复制代码

2.2.三、remove

删除指定位置的节点,须要传入参数index,和insert方法同样,先考虑索引的范围是否合法,而后考虑索引在边界时的操做,关键点是找到索引为index-1的这个节点,这个节点的next指向了要删除的节点。

this.remove = (index) => {
    if(index<0 || index>length) return
    if(index === 0) {
        let del_node = head
        head = head.next
        del_node.next = null
    } else {
        let del_index = 0
        let pre_node = null // 要删除节点的前一个节点
        let curr_node = head // 要删除的节点
        while(del_index<index) { // 依此循环找到
            del_index++
            pre_node = curr_node
            curr_node = curr_node.next
        }
        let del_node = curr_node
        pre_node.next = curr_node.next // 要删除节点的前一个节点的下一个节点等于要删除节点的下一个节点
        del_node.next = null // 要删除节点的下一个节点为空
        if(curr_node.next === null) { // 若是删除的是尾节点
            tail_node = pre_node
        }
    }
    length --
}
复制代码

2.2.四、最终代码

其余方法比较容易理解

function LinkList() {
  let Node = function (data) {
    this.data = data
    this.next = null
  }
  let length = 0
  let head = null
  let tail = null

  // 在尾部添加节点
  this.append = (data) => {
    // 建立新节点
    let new_node = new Node(data)
    if (head == null) {
      head = new_node
      tail = new_node
    } else {
      tail.next = new_node
      tail = new_node
    }
    length +=1
    return true
  }
  // 打印节点
  this.print = () => {
    let curr_node = head
    while (curr_node) {
      console.log(curr_node.data)
      curr_node = curr_node.next
    }
  }
  // 指定位置添加节点
  this.insert = (index, data) => {
    if (index > length || index < 0) {
      return
    } else if (index == length) {
      return this.append(data)
    } else {
      let new_node = new Node(data)
      if (index == 0) {
        new_node.next = head
        head = new_node
      } else {
        let insert_index = 1
        let curr_node = head
        while (insert_index < index) {
          insert_index ++
          curr_node = curr_node.next
        }
        let next_node = curr_node.next
        curr_node.next = new_node
        new_node.next = next_node
      }
    }
    length ++
    return true
  }
  // 删除指定位置节点
  this.remove = (index) =>{
    if (index<0||index>=length) {
      return false
    } else {
      let del_node = null
      if (index == 0) {
        del_node = head
        head = head.next
        del_node.next = null
      } else {
        let del_index = 0
        let pre_node = null
        let curr_node = head
        while (del_index<index) {
          del_index++
          pre_node = curr_node
          curr_node = curr_node.next
        }
        del_node = curr_node
        pre_node.next = curr_node.next
        if (curr_node.next == null) {
          tail = pre_node
        }
        del_node.next = null
      }
    }
    length --
    // return del_node.data
  }
  // 返回指定位置节点
  this.get = (index) => {
    if (index>=length || index<0) {
      return false
    }
    let node_index = 0
    let curr_node = head
    while (node_index<index) {
      node_index++
      curr_node = curr_node.next
    }
    return curr_node.data
  }
复制代码

三、链表应用

3.一、链表反转

迭代反转

思路

假设链表中间的某个点为curr_node,它的前一个节点是pre_node,后一个节点是next_node,如今把思路聚焦到这个curr_node节点上,只考虑在这一个点上进行翻转:curr_node.next = pre_node;只须要这简单的一个步骤就能够完成对curr_node节点的翻转,对于头节点来讲,它没有上一个节点,让 pre_node=null,表示它的上一个节点是一个空节点。在遍历的过程当中,每完成一个节点的翻转,都让curr_node = next_node,找到下一个须要翻转的节点。同时,pre_nodenext_node也跟随curr_node一块儿向后滑动。

function reveser(head) {
  if (!head) {
    return false
  }
  let pre_node = null
  let curr_node = head
  while (curr_node) { // 循环结束条件为当前节点为空
    let next_node = curr_node.next // 记录当前节点下一个节点
    curr_node.next = pre_node // 当前节点的下一个节点变为前一个节点
    pre_node = curr_node // 向下遍历
    curr_node = next_node
  }
  return pre_node
}
复制代码

递归反转

递归的核心之处在于先执行的后执行完,以及递归的出口

function reveser_digui(head) {
  if (!head) {
    return false
  }
  if (head.next == null) { // 出口
    return head
  }
  let new_head = reveser_digui(head.next) // 递归调用
  head.next.next = head // 下一个节点指向上一个节点
  head.next = null // 
  return new_head
}
复制代码

3.二、合并两个有序链表

已知有两个有序链表(链表元素从小到大),请实现函数merge_link,将两个链表合并成一个有序链表,并返回新链表,原有的两个链表不要修改。

思路

合并两个有序链表,是归并排序在链表上的一种实践。对两个链表,各自设置一个游标节点指向头节点,对游标节点上的数值进行比较,数值小的那个拿出来放入到合并链表中,同时游标节点向后滑动,继续比较游标节点数值大小。

为了实现滑动,须要使用一个while循环,当其中一个游标节点为null时,循环终止,这时,可能另外一个游标节点尚未到达尾节点,那么把这段尚未遍历结束的链表添加到合并列表上。

代码

function merge_link(head1,head2) {
    if(head1 === null && head2 === null) return
    if(head1 === null) {
        return head2
    } else if (head2 === null){
        return head1
    }
    let merge_head = null // 合并后的头节点
    let merge_tail = null // 合并后的尾节点
    let curr1 = head1 // 游标
    let curr2 = head2
    while(curr1&&curr2) {
        let min_node = null // 最小的节点
        if(curr1.data<curr2.data) { // 找到最小的节点
            min_node = curr1.data
            curr1 = curr1.next  // 向后滑动
        } else {
            min_node = curr2.data
            curr2 = curr2.next
        }
        // 想合并的链表添加节点
        if(merge_head === null) { // 链表为空
            merge_head = new Node(min_node)
            merge_tail = merge_head
        } else { // 不为空
            let new_node = new Node(min_node)
            merge_tail.next = new_node
            merge_tail = new_node
        }
        // 判断是否有剩余的部分
        let res_link = null
        if(curr_1){
            rest_link = curr_1;
        }
        if(curr_2){
            rest_link = curr_2;
        }
        while (res_link) { // 依此将剩余的加到合并链表
            let new_node = new Node(res_link.data)
            merge_tail.next = new_node
            merge_tail = new_node
            res_link = res_link.next
        }
    }
    return merge_head
}
复制代码

链表还有不少其余会被问道的问题好比:

  1. 查找单链表中的倒数第K个节点(k > 0):定义两个游标都指向head,先让其中一个走k步,而后两个一块儿走,当先走的走到尽头时此时后走的所在的位置就是倒数第k个。
  2. 查找单链表的中间结点:定义两个节点k一、k2,k1一次走两步,k2一次走一步,当k2走到尽头时此时k1所在的位置中间节点。
  3. 实现双向链表:多了一个前驱指针

四、最后

在学习了链表以后,发现链表比队列和栈更加困难,往后要多加复习和练习来巩固学到的内容。

相关文章
相关标签/搜索