链表

151 阅读4分钟

链表和数组的区别

参考 在聊这个问题之前,先看一下数据从逻辑结构上的分类。主要分为两类:线性表和非线性表。

线性表: 数据连成一条线的结构,今天要聊的链表和数组就属于这一类,除此之外还有栈,队列等。

非线性表: 数据之间的关系是非线性的,如树,堆,图等。

看完线性表和非线性表之后,来继续看下:

数组的定义:数据是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据

由于数组在内存中是连续存放的,所以通过下标来随机访问数组中的元素效率是非常高的。但与此同时,为了保证连续性,如果想在数组中添加一个元素,需要大量地对数据进行搬移工作。同理想在数组中删除一个元素也是如此。所以我们得出一个结论:在数组中随机访问的效率很高,但是执行添加和删除时效率低下,平均时间复杂度为O(n)。

链表的定义: 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

刚才介绍了在数组中添加和删除一个元素的效率是低下的。而链表的存储空间是不连续的,使用链表添加或者删除一个数据,我们并不需要为了保持内存的连续性而对数据进行搬移,所以在链表中添加和删除元素是非常高效的。但万事都有两面性,正因为链表的存储空间是不连续的,想要在链表中访问一个元素时,就无法像数组一样根据首地址和下标,通过寻址公式来计算出对应的节点。而只能通过指针去依次遍历找出相应的节点。所以我们得出一个结论:在链表中执行添加和删除操作时效率很高,而随机访问的效率很低,平均时间复杂度为O(n)。

单链表

创建单链表结构

// 声明链表节点
class Node {
  constructor (value) {
    this.val = value
    this.next = null
  }
}

// 声明链表的数据结构
class NodeList {
  constructor (arr) {
    // 声明链表的头部节点
    let head = new Node(arr.shift())
    let next = head
    arr.forEach(item => {
      next.next = new Node(item)
      next = next.next
    })
    return head
  }
}

let arr = [1,2,3]
let head = new NodeList(arr)

剑指从尾到头打印链表

leetcode

image.png
一次遍历,数组反转

var reversePrint = function(head) {
    let res = []
    next = head
    while(head) {
        res.push(next.val)
        if(next.next) {
            next = next.next
        }else {
            return res.reverse()
        }
    }
    return res 
};

递归回溯,终止条件为null的时候,因为visitor为同步,当head为null时,visitor就会完整执行,执行结束之后返回上一个visitor执行的地方继续往下执行nums.push(),执行完毕在返回上一个visitor,这样就形成了回溯。

var reversePrint = function (head) {
    let nums = []
    const visitor = function (head) {
        if (head !== null) {
            visitor(head.next)
            nums.push(head.val)
        }
    };
    visitor(head)
    return nums
}

利用栈先进后出的思想

var reversePrint = function(head) {
    const stack = [],res = [];
    let p = head,t = 0;
    while(p){
        stack.push(p.val);
        t ++;
        p = p.next;
    }
    for(let i = 0;i < t;i ++){
        res.push(stack.pop());
    }
    return res;
}

链表中倒数第k个节点

参考 image.png 思路:在链表中,链表的长度是未知的,如果需要获得长度就需要遍历一遍链表,而且链表一般不能直接用,而是需要借助临时变量这样才可以放心的对链表进行操作。而题目中要求返回链表,实际上链表就是一个指针,将当前位置的指针返回就是链表结构。

var getKthFromEnd = function(head, k) {
    let node = head, n = 0
    while(node){
        node = node.next
        n++
    }
    node = head
    for(let i = 0; i < n - k; i++) {
        node = node.next
    }
    return node
};

法二:快慢针
思路:题目中求倒数第n个,也就意味着长度是n。定义两个指针,令两个指针的距离为n。当最前面的指针走到头,后面的指针就指向这个链表的开始。

var getKthFromEnd = function(head, k) {
    let fast = head, low = head
    while(fast && k > 0) {
        fast = fast.next
        k--
    }
    while(fast) {
        fast = fast.next
        low = low.next
    }
    return low
};

反转链表

参考

image.png

法一:迭代
把链表的指针反转过来,指向上一个元素。
第一步:定义一个临时指针保存断开后后面的节点
第二步:让当前指针指向前驱节点

var reverseList = function(head) {
    let pre = null, curr = head
    while(curr) {
        let tmpNext = curr.next
        curr.next = pre
        pre = curr
        curr = tmpNext
    }
    return pre
}

法二:递归回溯
思路:首先利用递归,遍历到链表最末端,然后通过回溯赋值,将当前节点的next.next也就是下一个节点值指向指向自己,然后将自己的next指针指向null。

var reverseList = function(head) {
    if (head == null || head.next == null) {
        return head;
    }
    let newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
};

合并排序链表

题目

image.png
思路:
合并两个排序的链表,可以借鉴归并排序的思路,两个链表的头指针进行比较,将小的一边放入合并的链表。
在实际操作过程中,需要新创建一个链表可以使用题目中的链表结构创建,然后再赋值时应该考虑的是链表的指针的指向,让next直接等于下一个节点

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    let node = new ListNode(0)
    let head = node
    while(l1 && l2){
        if(l1.val <= l2.val) {
            node.next = l1
            l1 = l1.next
        }else if (l2.val < l1.val){
            node.next = l2
            l2 = l2.next
        }
        node = node.next
    }
    node.next =  l1 ? l1 : l2
    return head.next
};

删除链表重复值

题目

image.png 思路:想要删除链表的或者改变链表的值,只能改变链表的下一个指针的指向。所以在当前结点就判断下一个结点是否重复,重复了就让p.next = p.next.next,使当前的next指向下下个结点。没有重复就p.next.val的值存起来。

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function deleteDuplication(pHead)
{
    // write code here
    let obj = {}
    let p = pHead
    obj[p.val] = 1
    while(p.next) {
        if(obj[p.next.val]) {
           p.next = p.next.next
        }else {
            obj[p.next.val] = 1
            p = p.next
        }
    }
    return pHead
}

删除链表重复值2

题目 image.png

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function deleteDuplication(pHead)
{
    // write code here
    class ListNode {
        constructor(x, next) {
            this.val = x
            this.next = next
        }
    }
    
    const dummy = new ListNode(0, pHead) 
    let cur = dummy
    while(cur.next && cur.next.next) {
        if(cur.next.val === cur.next.next.val) {
            const tmp = cur.next.val
            while(cur.next && cur.next.val === tmp) {
                cur.next = cur.next.next
            }
        }else {
            cur = cur.next
        }
    }
    return dummy.next
}
module.exports = {
    deleteDuplication : deleteDuplication
};

循环链表

参考
循环链表和单链表相似,节点类型都是一样,唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性执行它本身,即

head.next = head;

这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表,如下图所示:

循环链表

class ListNode {
  constructor(x, head) {
    this.val = x
    this.next = head
  }
}

class creatList {
  constructor(arr) {
    let head = new ListNode(arr.shift(), null)
    let next = head
    arr.forEach(item => {
      next.next = new ListNode(item, head)
      next = next.next
    })
    return head
  }
}

let arr = [1,2,3]
let head = new creatList(arr)
for(let i = 0; i < 10; i++) {
  console.log(head.val)
  head = head.next
}

双向链表

参考

 //节点
 
function Node(element) {
    this.element = element;   //当前节点的元素
    this.next = null;         //下一个节点链接
    this.previous = null;         //上一个节点链接
}

//链表类

function LList () {
    this.head = new Node( 'head' );
    this.find = find;
    this.findLast = findLast;
    this.insert = insert;
    this.remove = remove;
    this.display = display;
    this.dispReverse = dispReverse;
}

//查找元素

function find ( item ) {
    var currNode = this.head;
    while ( currNode.element != item ){
        currNode = currNode.next;
    }
    return currNode;
}

//查找链表中的最后一个元素

function findLast () {
    var currNode = this.head;
    while ( !( currNode.next == null )){
        currNode = currNode.next;
    }
    return currNode;
}


//插入节点

function insert ( newElement , item ) {
    var newNode = new Node( newElement );
    var currNode = this.find( item );
    newNode.next = currNode.next;
    newNode.previous = currNode;
    currNode.next = newNode;
}

//显示链表元素

function display () {
    var currNode = this.head;
    while ( !(currNode.next == null) ){
        console.debug( currNode.next.element );
        currNode = currNode.next;
    }
}

//反向显示链表元素

function dispReverse () {
    var currNode = this.findLast();
    while ( !( currNode.previous == null )){
        console.log( currNode.element );
        currNode = currNode.previous;
    }
}

//删除节点

function remove ( item ) {
    var currNode = this.find ( item );
    if( !( currNode.next == null ) ){
        currNode.previous.next = currNode.next;
        currNode.next.previous = currNode.previous;
        currNode.next = null;
        currNode.previous = null;
    }
}

var fruits = new LList();

fruits.insert('Apple' , 'head');
fruits.insert('Banana' , 'Apple');
fruits.insert('Pear' , 'Banana');
fruits.insert('Grape' , 'Pear');

console.log( fruits.display() );        // Apple
                                        // Banana
                                        // Pear
                                        // Grape
                                        
console.log( fruits.dispReverse() );    // Grape
                                        // Pear
                                        // Banana
                                        // Apple