学习javascript 数据结构 之 链表

267 阅读9分钟

概述

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

链表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向NULL的指针。

类型分为单向、双向、循环链表等

  • 单向链表,特点是链表的链接方向是单向的。只能从头到尾遍历,只能找到后继无法找到前驱,增加删除节点简单,遍历时候不会死循环。
  • 双向链表,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。在查找元素可以反向查找前缀结点,一定程度上提升了查找数据元素的速度,但需要记录前缀结点,增加了额外的内存空间开销。
  • 循环链表,它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。在遍历的时候可以从任意结点开始,增加了遍历的灵活性。

链表 VS 数组:

  1. 删除、插入操作
    由于链表存储非连续的,删除、插入操作时不需要改变内存的地址,只需要修改结点的信息即可(包括指针指向,结点值)。而数组则需要搬移其他的元素(对最后一个元素插入或删除时才比较快)。所以链表性能上优于数组。如果数组是有序的,数组元素的删除操作就不一定比链表慢了。
  2. 查找操作
    数组大小固定不适合动态存储、动态添加,内存为一连续的地址,可随机访问,查询较快。而链表大小可变,扩展性强,只能顺着指针的方向查询,速度较慢。

链表实现

有以下函数:

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

    // 添加一个新元素
    this.append = function(data){
        // 创建新节点
        var node = new Node(data)
        // 如果是空链表,如果不为空则tail.next = node, 并让tail指向node
        if(head==null){
            head = node
            tail = head
        }else{
            // 尾节点指向新创建的节点
            tail.next = node
            // tail指向链表的最后一个节点
            tail = node        
        }
        // 长度加1
        length += 1            
        return true
    }

    // 返回链表大小
    this.length = function(){
        return length
    }

    // 获得指定位置的节点
    var getNode = function(index){
        if(index < 0 || index >= length){
            return null
        }
        var currentNode = head
        var nodeIndex = index
        while(nodeIndex > 0){
            currentNode = currentNode.next
        }
        return currentNode
    }

    // 在指定位置插入新的元素
    this.insert = function(index, data){
        // index == length,说明是在尾节点的后面新增,直接调用append方法即可
        if(index == length){
            return this.append(data)
        }else if(index > length || index < 0){
            // index范围错误
            return false
        }else{
            var newNode = new Node(data)
            if(index == 0){
                // 如果在头节点前面插入,新的节点就变成了头节点
                newNode.next= head
                head = newNode
            }else{
                // 要插入的位置是index,找到索引为index-1的节点,然后进行连接
                var preNode = getNode(index-1)
                newNode.next = preNode.next
                preNode.next = newNode
            }
            length += 1
            return true
        }
    }

    // 删除指定位置的节点
    this.remove = function(index){
        // 参数不合法,如果index< 或者 index>=length,索引都是错误的,返回null
        if(index < 0 || index >= length){
            return null
        }else{
            var deleteNode = null
            // 如果index==0,删除的是头节点,只需要执行head = head.next就可以把头节点删除。
            if(index == 0){
                // head指向下一个节点
                deleteNode = head
                head = head.next
                // 如果head == null,说明之前链表只有一个节点
                if(!head){
                    tail = null
                }
            }            
            // 如果index > 0,那么就找到索引为index-1的节点,利用这个节点将索引为index的节点删除
            else{
                // 找到索引为index-1的节点
                var preNode = getNode(index-1)
                deleteNode = preNode.next
                preNode.next = preNode.next.next
                // 删除节点时,如果被删除的节点是尾节点,tail要指向新的尾节点
                if(deleteNode.next==null){
                    tail = preNode
                }
            }

            length -= 1
            deleteNode.next = null
            return deleteNode.data
        }
    }

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

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

    // 返回指定位置节点的值
    this.get = function(index){
        var node = getNode(index)
        if(node){
            return node.data
        }
        return null
    }

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

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

    // 返回指定元素的索引,如果没有,返回-1。有多个相同元素,返回第一个
    this.indexOf = function(data){
        var index = -1
        var currentNode = head
        while(currentNode){
            index += 1
            if(currentNode.data == data){
                return index
            }else{
                currentNode = currentNode.next
            }
        }
        return -1
    }

    // 输出链表
    this.print = function(){
        var currentNode = head
        var strLink = ''
        while(currentNode){

            strLink += currentNode.data.toString() + ' ->'
            currentNode = currentNode.next;
        }
        strLink += 'null'
        console.log(strLink)
        console.log('长度为'+ length.toString())
    }

    // isEmpty
    this.isEmpty = function(){
        return length == 0
    }

    // 清空链表
    this.clear = function(){
        head = null
        tail = null
        length = 0
    }
}

题型

环形链表

思路

快慢指针。定义2个指针,一个慢指针、一个慢指针,并且一开始慢指针指向head节点,快指针指向head.next节点,然后快指针每次向前移动2步,慢指针每次向前移动1步,开始遍历链表。如果链表有环,那么快慢指针一定会相遇,指向同一个节点,当指向同一个节点时,遍历结束。

代码实现
function ListNode(val){
    this.val = val
    this.next = null
}

const hasCycle = head => {
    if(!head) return false

    let pre = head, cur = head

    while(cur && cur.next){
        pre = pre.next
        cur = cur.next.next
        if(pre === cur){
            return true
        }
    }

    return false
}

环形链表2

const detectCycle = (head) => {
    if(!head) return null
    let pre = head, cur = head

    while(cur && cur.next){
        pre = pre.next
        cur = cur.next.next
        if(pre === cur){
            let temp = head
            while(pre !== temp){
                pre = pre.next
                temp = temp.next
            }
            return pre
        }
    }
    return null
}

反转链表

思路

定义指针pre、cur、next, pre指向null,cur指向我们的头节点,next指向cur所指向节点的下一个节点。 指针初始化完毕,然后重复上述操作,当cur指针指向null的时候,我们就完成了整个链表的反转 0f42fbf6f4b220c3231e8c0bdc5d76a.png

813cb1a090696b03df92e71407c1685.png

d4219d6dc1c566ed3770be568e97732.png

d5283d455239effe8f28daa28b3aebd.png

e7786d6dacb714ca6bc329b383e8350.png

代码实现

const reverseList = head => {
    if(!head) return null

    let pre = null, cur = head
    while(cur){
        [cur.next, pre, cur] = [pre, cur, cur.next]
    }
    return pre
}

反转链表2

思路

1637992269(1).png 需要将第m个节点到第n个节点的链表进行反转。例如m=2,n=4

cfeb5f2031184513859a85b83adc89a.png 定义一个虚拟头节点,命名叫做hair,将它指向我们的真实头节点。

f64eb2f67981dc31f2acd0558a90813.png 定义一个指针pre指向虚拟头节点。

254b02e085c8bcfae13d24a9628bb76.png 定义一个指针cur指向pre指针所指向节点的下一个节点。

bb804a174deda1e4a1fb9a8f1f83893.png pre指针和cur指针同时向后移动,直到找到第m个节点。

19760a3a22996c600fe0420ab32288e.png 定义指针con和tail,con指向pre所指向的节点,tail指向cur指针所指向的节点。

d5201493ddabec68650eeed67252092.png con所指向的节点,将是我们将部分链表反转后,部分链表头节点的前驱节点。 tail则是部分链表反转后的尾节点。

6728e6ef605f6b14c3e44edea887170.png 开始链表反转,首先定义一个指针third指向cur所指向的节点的下一个节点。

70e74c1a4d9a6dd34de92930a7dc046.png 然后将cur所指向的节点指向pre所指向的节点,将pre指针移动到cur指针所在的位置。

3528b2ecd1498220f86c18ef6db8045.png 将cur指针移动到third指针所在的位置,直到我们的pre指针指向第n个节点,重复上面步骤。 此时pre指针指向了第m个节点并且将第m到第n个节点之间反转完毕。

486c39638fc38939a4b0561e2f4fc03.png

a93bdca6997b3c2be0be4feaca39b87.png

b692b4926036aa57db4bd151ed3e3fd.png

1ddbee2dc4665366899290fdff982ac.png 将con指针所指向的节点指向pre指针指向的节点。

0497931b639aaf498f69fbc11f1b16f.png 将tail指针所指的节点指向cur指针所指的节点。

08e19f84fcf0773d04a84a44cafda10.png 显示最终链表。

代码实现

const reverseBetween = (head, left, right) =>{
    if(!head) return null

    let ret = new ListNode(-1, head), pre =ret, cnt = right - left + 1
    while(--left){
        pre = pre.next

    }
    pre.next = reverse(pre.next, cnt)

    return ret.next

}
const reverse = (head,n) =>{
    let pre = null, cur = head
    while(n--){
        [cur.next, pre, cur] = [pre, cur, cur.next]
    }

    head.next = cur
    return pre
}

K个一组反转链表

const reverseKGroup = (head, k) => {
    if(!head) return null
    let ret = new ListNode(-1, head), pre = ret

    do{
        pre.next = reverse(pre.next, k)
        for(let i=0;i< k && pre; i++){
            pre = pre.next
        }
        if(!pre) break;
    }while(1);

    return ret.next
}

const reverse = (head,n) => {
    let pre = head, cur = head, con = n
    while(--n && pre){
        pre = pre.next
    }
    if(!pre) return head
    pre = null
    while(con--){
        [cur.next, pre, cur] = [pre, cur, cur.next]
    }
    head.next = cur
    return pre
}

快乐数

思路

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为1,也可能是无限循环但始终变不到1。
  • 如果可以变为1,那么这个数就是快乐数。
  • 如果n是快乐数就返回 true,否则返回 false。

代码实现

const isHappy = n => {
    let pre = n, cur = getNext(n)
    while(cur !== pre && cur !==1){
        pre = getNext(pre)
        cur = getNext(getNext(cur))
    }
    return cur === 1 
}

const getNext = (n) => {
    let t = 0
    while(n){
        t+=(n%10) * (n%10)
        n = Math.floor(n/10)
    }
    return t
}

删除链表的倒数第N个结点

思路

  • 通过双指针找到倒数第n个节点 back,back 的前一个节点 prev。
  • 将 prev 的下个节点指向 back 的下个节点。
  • 如果 prev 为空则代表删除头节点,返回 head.next 即可。
  • 如果 prev 不为空时返回head。

代码实现

const removeN = (head, n) => {
  let front = back = head
  let prev = null

  // 指针从头走 n-1 步找到第 n 个节点
  while (--n) {
    front = front.next
  }

  // 双指针同时走,前面指针到达链表位节点时,后指针为倒数第 n 个节点,记录第 n 个节点的前一个节点 prev
  while (front.next) {
    front = front.next
    prev = back
    back = back.next
  }

  //如果 prev 为空则表示删除头节点
  if (!prev) return head.next

  //删除 back 节点,即将 back 的前节点指向 back 的后一个节点
  prev.next = back.next

  return head
}

删除排序链表中的重复元素

思路

遍历链表,如果发现当前元素和下一个元素值相同,则删除下一个元素值。

代码实现

const deleteDuplicates = (head) => {
  let p = head
  while (p && p.next) {
    if (p.val === p.next.val) {
      p.next = p.next.next
    } else {
      p = p.next
    }
  }
  return head
}

删除排序链表中的重复元素2

思路

  • 链表已经过升序排列,所以只需要比较当前节点和下一个节点的值的大小,但是这道题比较难一点,新增变量isDup记录是否删除当前节点。

  • 可能头指针需要删除,为了避免每次循环进行判断,在头指针前面插入一个节点result

  • 待会儿要返回整个链表,所以找个需要找个代跑prev,以保留result头指针的位置。

  • 每次只需要比较当前节点prev.next和下个节点prev.next.next是否相同即可。

  • 如果当前节点与下一个节点的值相同,删除下一个元素,同时标记一下isDup = true, 先继续匹配还有没相同的值, 如果没有则下一轮删除掉当前节点。

代码实现

const deleteDuplicates = (head) => {
    const result = new ListNode(0, head)
    let isDup = false, prev = result

    // prev.next指当前节点, prev.next.next指下个节点
    while(prev.next && prev.next.next) {
        if (prev.next.val === prev.next.next.val) {
            // 标记,待会儿删除当前节点
            isDup = true
            // 删除下个节点
            prev.next.next = prev.next.next.next
        } else {
            // 判断是否需要删除当前节点
            if (isDup) {
                prev.next = prev.next.next
                isDup = false
            } else {
                prev =  prev.next
            }
        }
    }

    // 循环结束, 判断是否有需要删除当前节点
    if (isDup) {
        prev.next = null
    }

    return result.next
}