算法学习笔记之数据结构——链表

197 阅读3分钟

基本概念

相较于只适合在表尾插入和删除元素的顺序表,链表是一种能快速在任意位置插入和删除元素的链式存储结构。

常见的链表主要有单链表、双向链表、循环链表和双向循环链表等。

基本应用

从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @return {number[]}
 */
var reversePrint = function(head) {
    let res = []
    
    //一边遍历节点一边调用Array.unshift()方法将链表中的结点值插入到res数组的前面
    while(head){
        res.unshift(head.val)
        head = head.next
    }
    return res
};

删除链表的节点

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

思路:

设置一个哨兵节点来指向头指针,后面我们就可以用这个哨兵节点找到头指针。

从哨兵节点开始遍历,直到找到要删除的节点,执行删除结点操作,并退出循环。

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var deleteNode = function(head, val) {
    //设置哨兵节点并指向头指针
    let pre = new ListNode(-1)
    pre.next = head	
    let node = pre
    while(node.next){
         if(node.next.val == val){
           //删除结点操作
           node.next = node.next.next
           break
         }
        node = node.next
	}
    return pre.next
}

反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var deleteNode = function(head, val) {
    //设置哨兵节点并指向头指针
    let pre = new ListNode(-1)
    pre.next = head	
    let node = pre
    while(node.next){
         if(node.next.val == val){
           //删除结点操作
		   node.next = node.next.next
           break
         }
        node = node.next
	}
    return pre.next
}

复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

方法一:

利用map来模拟哈希表,

首先遍历链表并将结点作为键,将复制结点作为键值存入哈希表。

再次遍历链表,读取出复制节点并为复制结点的next和random属性赋值。

由于节点的nextrandom属性都是指向链表中的某个节点,故利用map.get()方法访问这个两个属性,实际上是访问这两个被指向的结点的复制结点

 * // Definition for a Node.
 * function Node(val, next, random) {
 *    this.val = val;
 *    this.next = next;
 *    this.random = random;
 * };
 */

/**
 * @param {Node} head
 * @return {Node}
 */
var copyRandomList = function(head) {
    if(!head) return head
    let map = new Map()
    let node = head
    
    //遍历链表并将结点作为键,将复制结点作为键值存入哈希表
    while(node){
		map.set(node,new Node(node.val))
        node = node.next
    }
    node = head
    
    //再次遍历链表,读取出复制节点并为复制结点的next和random属性赋值
    while(node){
	map.get(node).next = node.next?map.get(node.next):null
        
        map.get(node).random = node.random?map.get(node.random):null
        
        node = node.next
    }
    return map.get(head)
}

时间复杂度:O(n),其中 n 是链表的长度。对于每个节点,我们至多访问其 next 和 random 各一次,均摊每个点至多被访问两次。

空间复杂度:O(n),其中 n 是链表的长度。为哈希表的空间开销。

方法二

迭代 + 结点拆分。

在链表中的每个结点后插入其复制结点;

such as:

1->2->3 => 1->1->2->2->3->3

然后我们遍历链表,分别为复制结点的next和random属性赋值。

   if(!head) return
   let node = head
   
   //在链表中,每个结点后插入该结点的复制结点
   while(node){
       let next = node.next
       node.next = new ListNode(node.val,node.next,null)
       node = next
   }
   node = head
   let copyNode = null 
   
   //给复制结点的random赋值
   while(node){
       let next = node.next.next
       copyNode = node.next
       copyNode.random = node.random ? node.random.next : null
       node = next
   }
   node = head
   //获取复制链表的头结点
   let res = head.next
   
   //给复制的结点的next赋值
   while(node){
       let next = node.next.next
       copyNode = node.next
       copyNode.next = next ? next.next : null
       node.next = next
       node = next
   }
   return res
}

时间复杂度O(n),n 是链表的长度。我们只需要遍历该链表三次。

空间复杂度O(1)