js数据结构之链表

362 阅读8分钟

链表的概述

链表:

  • 链表中的元素在内存中不必是连续的空间
  • 链表中的每个元素由一个存储元素本身的节点和指向下一个元素的引用组成


链表和数组一样,可以用来存储一系列的元素;但是数组有一些缺点:数组的创建通常需要申请一段连续的内存空间,并且大小是固定的;并且在数组开头或者中间位置插入数据的成本很高,需要大量元素的位移;


相对于数组,链表有一些优点

  1. 内存空间不是必须连续的,可以充分利用计算机的内存,实现内存的动态管理
  2. 在创建的时候不需要确定大小,并且可以无线延伸下去
  3. 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率比较高

链表的缺点

  • 链表访问任何一个位置的元素时,都需要从头开始访问,无法跳过第一个元素访问任何一个元素
  • 链表通过下标直接访问元素的时候,也需要从头一个个访问,直到找到该元素;


链表相当于火车结构,所以创建链表的时候,要保存两个属性,一个是链表的长度,一个是链表中的第一个节点;


链表的常见操作

  • append(element):向列表尾部添加一个新的项
  • Insert(position,element):向列表的特定位置插入一个新的项
  • get(position):获取对应位置的元素
  • indexOf(element):返回元素在列表中的索引,如果列表中没有该元素则返回-1
  • update(position):修改某个位置的元素
  • removeAt(position):从列表的特定位置移除一项
  • remove(element):从列表中移除一项
  • isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false;
  • size():返回链表包含的元素的个数,与数组的length属性类似;
  • toString():由于列表项使用了Node类,需要重写继承自Javascript对象默认的toString方法,让其只输出元素的值;


链表常见的算法题:

1.反转链表

  • 简单的反转链表

思路递归解决方案循环解决方案,但是需要注意的是,一定要保存后序节点。我们很容易将当前节点的 next指针直接指向前一个节点,但其实当前节点下一个节点的指针也就丢失了。因此,需要在遍历的过程当中,先将下一个节点保存,然后再操作next指向。

//循环解决方案:
let reverseList =  (head) => {
    if (!head)
        return null;
    let pre = null, cur = head;
    while (cur) {
        // 关键: 一定要保存下一个节点的值
        let next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
};
//循环解决方案
let reverseList = (head) =>{
  let reverse = (pre, cur) => {
    if(!cur) return pre;
    // 保存 next 节点
    let next = cur.next;
    cur.next = pre;
    reverse(cur, next);
  }
  return reverse(null, head);
}

2.从尾到头打印链表

输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

思路:首先需要遍历链表,遍历的顺序是从头到尾,输出的顺序是从尾到头;也就是说,第一个遍历到的节点最后一个输出,最后遍历到的节点第一个输出,这就是典型的“后进先出”,我们可以利用栈实现这种顺序。每经过一个节点的时候,就把该节点放到一个栈中,当遍历完整的链表,再从栈顶开始逐个输出节点的值。unshift()向开头添加一个或者多个元素

function printListFromTailToHead(head){var res=[],pNode=head;
while(pNode != null){
    res.unshift(pNode.val);
    pNode = pNode.next;
  }
  return res;
}


3.链表合并

  • 合并两个有序链表:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
可以使用递归完成:
//递归实现
var mergeTwoLists = function(l1, l2) {
    const merge = (l1, l2) => {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val > l2.val) {
            l2.next = merge(l1, l2.next);
            return l2;
        }else {
            l1.next = merge(l1.next, l2);
            return l1;
        }
    }
    return merge(l1, l2);
};
  • 合并K个有序链表:可以使用上一题的递归合并两个有序链表+分治算法

思路:将多个链表不断分割,从中间不断分割,也就是将数组中的链表分治,不断将数组中的链表中间划分,分别合并,然后整体再合并成一个大链表;

/**
  * 功能:合并 k 个链表
  * 边界条件:
  * 1)判断数组是否为空
  * 2)判断数组长度为 1 时
  * 3)判断数组长度为 2 时
  * 4)判断数组长度大于 2 时
  */
var mergeKLists = function(lists) {
    // 当 lists 中没有链表时
    if(lists.length == 0){
        return null;
    }else if(lists.length == 1){
        // 判断数组长度为 1 时
        return lists[0];
    }else if(lists.length == 2){
        // 判断数组长度为 2 时
        return mergeTwoLists(lists[0],lists[1]);
    }else{
        // 判断数组长度大于 2 时
        // 取数组的中间坐标
        let middle = Math.floor(lists.length/2);
        // 取左右两边数组
        let leftList = lists.slice(0,middle);
        let rightList = lists.slice(middle);
		// 递归、分割、合并
        return mergeTwoLists(mergeKLists(leftList),mergeKLists(rightList));
    }       
};
//两个链表合并
var mergeTwoLists = function(l1, l2) {
    let result = null;
 
    //终止条件
    if(l1 == null) return l2;
    if(l2 == null) return l1;
 
    //判断数值大小递归
    if(l1.val < l2.val){
        result = l1;
        result.next = mergeTwoLists(l1.next,l2);
    }else{
        result = l2;
        result.next = mergeTwoLists(l2.next,l1);
    }
    
    //返回结果
    return result;
};   



扩展:分治算法:把一个复杂的问题分成两个或更多的相同或者相似的子问题;再把子问题分成更小的子问题….直到最后子问题可以简单的直接求解;

由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。


4.删除节点:

  1. 删除链表中的节点
  • 请编写一个函数,使其可以删除某个链表中给定的节点,你将只被给定要求被删除的节点。
  • 现有一个链表 – head = [4,5,1,9],它可以表示为:

在这里插入图片描述

示例1:

输入: head = [4,5,1,9], node = 5

输出: [4,1,9]

解释: 给定你链表中值为 5 的第二个节点,那么在调用了函数之后,该链表应变为 4 -> 1 -> 9.

var deleteNode =
function(node) { //因为要删除节点,所以我们将节点的下一个节点赋值给nextNode    
var nextNode = node.next;    
//将下个节点的值赋给节点    
node.val = nextNode.val;//将下个节点的指向赋给节点    
node.next = nextNode.next; 
};


2.删除链表中的重复节点

重新比较连接数组---链表是排好顺序的,所以重复元素会相邻出现,递归链表;

1.当前节点或者当前节点的next为空,就返回这个节点;

2.当前节点是重复节点:找到后面第一个不重复的节点;

3.当前节点不重复:将当前的节点的next赋值为下一个不重复的节点;

思路:

function deleteDuplication(pHead){      
if(!pHead || !pHead.next){        
        return pHead;      
    }else if(pHead.val === pHead.next.val){
        let tempNode = pHead.next;
        while(tempNode && pHead.val === tempNode.val){
          tempNode = tempNode.next;
        }        
        return deleteDuplication(tempNode);
      }else{
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
      }
    }

5.环形链表

题目:

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null

思路:采用双指针解法,一快一慢指针;快指针每次跑两个element,慢指针每次跑一个element;如果存在一个圈,那么两个指针一定会相遇的;

  • 下图所示,我们先找到快慢指针相遇的点,p。我们再假设,环的入口在点q,从头节点到点q距离为A,q p两点间距离为B,p q两点间距离为C。
  • 因为快指针是慢指针的两倍速,且他们在p点相遇,则我们可以得到等式 2(A+B) = A+B+C+B. (慢指针的2倍距离正好等于快指针走的距离,快指针绕着b+c周长的圆圈走的---【慢指针走了a+b,快指针走了2a+2b;也就是说a+b的长度正好是环一圈大小的倍数,而环周长是b+c,所以得出,a=c】;)
  • 由3的等式,我们可得,C = A。
  • 这时,因为我们的slow指针已经在p,我们可以新建一个另外的指针,slow2,让他从头节点开始走,每次只走下一个,原slow指针继续保持原来的走法,和slow2同样,每次只走下一个。
  • 我们期待着slow2和原slow指针的相遇,因为我们知道A=C,所以当他们相遇的点,一定是q了。
  • 我们返回slow2或者slow任意一个节点即可,因为此刻他们指向的是同一个节点,即环的起始点,q;


function EntryNodeOfLoop(pHead){
  if(pHead == null){
      return null;
  }
    if(pHead.next == null){
 return null;
 }  
var slow = pHead;var fast = pHead;
  while(slow != null && fast.next != null){
 slow = slow.next; fast = fast.next.next;
  if(slow == fast ) break;  
}  
var p1 = slow; var p2 = pHead;
 while(p1 != p2){
  p1 = p1.next;
 p2 = p2.next;
  }//最后返回任何一个都行;
 return p2; 
}

6.两个链表的公共节点

题目:输入两个链表,找出它们的第一个公共节点?

方法:两个指针扫描两个链表,最终两个指针到达null或者到达公共节点;

原 如 图 所 示 ! 
A: 
B: 
4 
b 
0 
1 
1 
pA:a+c+b 
pB: b+C+a 
8 
4 
5 
5 
和 始 化 , headA, pg 
, headg 。 科 始 返 历 。 
以 上 为 、 PA 会 先 到 达 喪 、 当 PA 到 达 末 呈 时 , *WPA 为 headB; 同 样 的 , 当 p 日 到 达 末 尾 时 , 雨 置 为 
headA• 当 PA 与 弗 相 时 , 必 然 就 是 两 个 地 点 。 
为 什 么 要 这 处 ? 因 为 这 样 的 一 个 返 历 过 程 . 对 PA 而 言 . 走 过 的 程 即 为 a.c+b . 对 p 巳 而 言 . 即 为 ( 艹 . 显 然 
a.c.b = b.c.a , 谅 就 是 该 算 法 的 心 0 。 
即 使 两 个 表 没 有 楣 交 点 , 事 实 上 , 仍 然 可 以 统 一 处 理 , 因 为 这 情 况 味 着 相 凳 点 就 是 nun , 也 是 上 图 中 的 公 共 部 
分 [ 有 了 . 从 而 准 式 成 了 PA: a*b . : b+a . 这 同 是 咸 立 的 。

相当于 链表1=A + C;链表2=B+C

A+C -> B+C

B+C -> A+C--------如果有公共节点,最后一定会有重复的地方

代码实现:

function FindFirstCommonNode(pHead1,pHead2){  
var p1 = pHead1;  
var p2 = pHead2;  
while(p1 != p2){    
//检查p1和p2是否走到最后一位,如果走到的话,就让p1指向pHead2;p2指向phead1;
    p1 = p1 == null?pHead2:p1.next;
    p2 = p2 == null?pHead2:p2.next;
  }  
return p1;
}