基础班第四课 链表 (左神算法听课笔记)

本章知识点:

  • 介绍几个Java中两个常用的数据结构的 用法 +性能(不介绍原理)
  • 哈希表(HashSet HashMap)
  • 有序表(TreeSet TreeMap)
  • 链表

Part 0 : 哈希表

在使用层面上可以理解成一种集合结构
在Java中,哈希表有( key , value ),其中key是必须的主要的,value是附带的,要不要都可以。具体来说有HashSet和HashMap两种。下面分别介绍这两种数据结构:
在这里插入图片描述

Part 1 : 有序表在这里插入图片描述

常用操作:在这里插入图片描述

本章中使用的链表结构:

//单链表
Class Node<V> {
	V value;
	Node next;
}

//双链表
Class Node<V> {
	V value;
	Node next;
	Node last;
}

Part 2 : 链表基础题

反转单向和双向链表

要求:链表长度为n,时间复杂度要求为O(n),空间复杂度要求O(1)

//单链表
public static class Node {  
    public int value;  
    public Node next;  
  
    public Node(int data) {  
        this.value = data;  
    }  
}  
  
public static Node reverseList(Node head) {  
    //使用两个指针,一个之前逆序之后的后置,一个指向逆序之后的前置节点  
    //要求额外空间复杂度是O(1),所以只能直接在原链表上面逆序  
    Node pre = null;  
    Node next = null;  
  
    while(head != null) {  
        next = head.next; //存一下next,一会要断了  
  
        head.next = pre;  //逆序  
  
        pre = head;       //大家向后走一步  
        head = next;  
        //每次结束的时候,head会和next在一起,然后pre在原序前一个  
    }  
    return pre;  
}
//双链表
public static class DoubleNode {  
    public int value;  
    public DoubleNode last;  
    public DoubleNode next;  
  
    public DoubleNode(int data) {  
        this.value = data;  
    }  
}  
  
public static DoubleNode reverseList(DoubleNode head) {  
    //和单链表逆序是一样的,只是要多更新一个last指针而已  
    DoubleNode pre = null;  
    DoubleNode next = null;  
  
    while(head != null) {  
        next = head.next;//存一下next,一会要断了  
  
        head.next = pre; //逆序  
        head.last = next;  
  
        pre = head;//大家向后走一步  
        head = next;  
    }  
    return pre;  
}

打印两个有序链表的公共部分

已知两链表的长度之和为N
要求时间复杂度为O(N),额外空间复杂度为O(1)

思路很简单,因为是有序链表,要求遍历一遍,不使用额外数组王成公共部分打印。
那就谁小谁向后走,大的不走,遇到一样就打印然后一起向后走。

public static void printCommonPart(Node head1, Node head2) {  
    System.out.print("Common Part: ");  
    while(head1 != null && head2 != null) {  
        if(head1.value < head2.value) {  
            head1 = head1.next;  
        }else if(head2.value < head1.value) {  
            head2 = head2.next;  
        }else {  
            System.out.print(head1.value + " ");  
            head1 = head1.next;  
            head2 = head2.next;  
        }  
    }  
    System.out.println();  
}

🤔❓ 关于链表题目:
对于笔试:不用在空间复杂度,写出来就OK,你直接使用Hash表把链表copy一份都OK
对于面试:为了体现出区分度,还是要考虑空间复杂度,毕竟链表的优化难就难在空间如何节省。

Part 3 使用优化技巧的链表难题

📖 在链表的题目中有两个重要的基本技巧:
->使用额外数据结构(哈希表)
->快慢指针(解决99%的链表难题)

判断链表是否是回文结构

什么是回文结构?
前后对称的结构,例如:1->2->2->1 , 4->5->4, 1 这样子的
或者说,逆序之后结构不改变。
或者说,以链表对称轴为分界的两部分,前部的逆序 = 后部。

要求:时间复杂度O(N),空间复杂度O(1)

思路详解:可以有三种实现方法
方法一:需要O(n)的额外空间

方法二:需要O(n/2)的额外空间
因为回文结构有“前部的逆序 = 后部”,那么可以考虑,前部全部进栈,然后一个个读入后部,如果遇到相同的数字就弹出,遇到不相同的数字就返回false,最后栈为空+读完所有数字就返回true。

🤔❓ 难点在于,怎么找到链表的对称轴的位置呢?
这就要使用技巧快慢指针
快慢指针就是,满指针正常一个个遍历,但是快指针一次走多步(这里要找中点所以是两步)。
这样当快指针走完的时候,慢指针就正好在中点位置。

方法三:需要O(1)的额外空间
使用链表逆序还将后半部分逆序,mid节点next = null。然后从头和尾比较(头向后走,尾向前走),最后再将链表逆序恢复原状。

// need O(1) extra space  
//  x -> x  -> x  -> x -> x -> x -> x  
//后半部分逆序,对称轴位置指向null  
//  x -> x  -> x  -> x(.next=null) <- x <- x <- x  
//然后头尾遍历比较  
//最后恢复  
//  x -> x  -> x  -> x -> x -> x -> x  
public static boolean isPalindrome3(Node head) {  
    if(head == null || head.next == null) {  
        return true;  
    }  
    //使用快慢指针找到末尾和中点  
    Node n1 = head;  
    Node n2 = head;  
    while(n2.next != null && n2.next.next != null) {  
        n1 = n1.next;  
        n2 = n2.next.next;  
    }  
  
    //后半部分做链表逆序(n2相当于head n1相当于pre n3相当于next)  
    //逆序的尾节点是mid中点 头节点是原链表的最后一个节点  
    n2 = n1.next;  
    n1.next = null;  
    Node n3 = null;  
  
    while(n2 != null) {  
        n3 = n2.next;  
        n2.next = n1;  
        n1 = n2;  
        n2 = n3;  
    }  
  
    n3 = n1; // n3 -> save last node(恢复的时候要用捏)  
    n2 = head;// n2 -> left first node  
  
    //开始比较前后两个部分  
    //这时候的位置:n2在head n1在尾部 n3在mid  
    boolean check = true;  
    while(n1 != null && n2 != null) {  
        if(n1.value != n2.value) {  
            //不直接返回是因为后面还要把链表还原!  
            check = false;  
            break;        }  
        n1 = n1.next;  
        n2 = n2.next;  
    }  
  
    n1 = n3.next;  
    n3.next = null;  
    //还原链表 还是链表逆序  
    //这时候的位置:n1在后链表头(相当于head) n2是next n3在中点是pre  
    while (n1 != null) {  
        n2 = n1.next;  
        n1.next = n3;  
        //n3 = n2;  形成环路了 错误了
        n3 = n1;  
        n1 = n2;  
    }  
    return check;  
}  
public static void printLinkedList(Node node) {  
    System.out.print("Linked List: ");  
    while (node != null) {  
        System.out.print(node.value + " ");  
        node = node.next;  
    }  
    System.out.println();  
}

将单向链表考按照某值划分成左边小,中间相等,两边大的形式

输入:整数pivot
单链表头节点head
实现:一个调整链表函数,左边部分小于pivot,中间部分等于pivot,右边部分大于pivot。

进阶要求:
实现排序的稳定性
要求时间复杂度O(N) 额外空间复杂度为O(1)
原来使用链表可以实现快排的稳定性吗?链表荷兰国旗问题

过程图示
思路比较简单,就是coding。
分成是那个部分,每个部分都使用两个指针(头部SH ST初始化为null)保存部分的头和尾位置。
遍历一遍,一遍把节点串到对应的部分。
最后再把三个部分连接就好了。

⚠️ 要注意的点是:
有可能三个部分可能是空的,所以要判断边界条件。
如果H = T = null的话,就证明这个部分不存在。

coding:

public class Node {  
    int value;  
    Node next;  
  
    public Node(int data) {  
        this.value = data;  
    }  
}  
public static Node listPartition2(Node head, int pivot) {  
    Node SH = null;  
    Node ST = null;  
    Node EH = null;  
    Node ET = null;  
    Node BH = null;  
    Node BT = null;  
  
   while(head != null) {  
       if(head.value<pivot) {  
           if(SH==null) {  
               SH = head;  
               ST = head;  
           }else {  
               ST.next = head;  
               ST = ST.next;  
           }  
  
           if(head.value==pivot) {  
               if(EH==null) {  
                   EH = head;  
                   ET = head;  
               }else {  
                   ET.next = head;  
                   ET = ET.next;  
               }  
               if(head.value>pivot) {  
                   if(BH==null) {  
                       BH = head;  
                       BT = head;  
                   }else {  
                       BT.next = head;  
                       BT = BT.next;  
                   }  
       }  
   }  
}
另外两个使用栈的方法以后再补充(未完待续)

复制含有随机指针的链表

在题目中定义了一种特殊的链表节点:

Node {
	int value;
	Node next;
	Node rand;  //除了一般链表都有的next指针还有一个随机指针
	
	Node(int val) {
		this.value = val;
	}
}

⚠️ rand指针可以指向任何节点(包括节点本身,也可以指向null)

要求:时间复杂度O(N) 空间复杂度O(1);

两种思路:
思路一
比较简单暴力的方法,就是直接使用一个HashMap将每一个节点的rand指针指向的节点保存下来,然后再根据这个HashMap中的数据来复制。

思路二
考虑空间复杂度:

  • 在旧节点的next生成新节点(新节点的next是老节点原来的next,老节点的next是新节点)
  • 然后遍历整个链表,并再克隆老节点的rand指针到新节点上。
  • 最后再调整next指针恢复老链表,独立出新链表。

coding:

//直接使用Hash表来存下整个链表的结构  
//HashMap map <Node, Node> 其中key是旧节点,value是复制出的新节点。  
public static Node copyListWithRand1(Node head) {  
    HashMap<Node, Node> map = new HashMap<>();  
  
    //初始化map:原节点为key,并创建新节点作为value  
    Node cur = head;  
    while(cur != null) {  
        map.put(cur,new Node(cur.value));  
        cur = cur.next;  
    }  
  
    //再遍历一次复制指针部分  
    cur = head;  
    while(cur != null) {  
        map.get(cur).next = map.get(cur.next);  
        map.get(cur).rand = map.get(cur.rand);  
        cur = cur.next;  
    }  
    return map.get(head);  
}  
  

}
public static Node copyListWithRand2(Node head) {  
    if(head == null) {  
        return null;  
    }  
  
    Node cur = head;  
    Node next = null;  //用来记录老节点的next  
    //创建新节点,挂在老节点屁股后面  
    while (cur != null) {  
        next = cur.next;  
        cur.next = new Node(cur.value);  
        cur.next.next = next;  
        cur = next;  
    }  
  
    //设置新节点的rand指针  
    cur = head;  
    Node copyNode = null;  
    while(cur != null) {  
        next = cur.next.next; //这里的next是下一个老节点  
        copyNode = cur.next;  
        copyNode.rand = cur.rand != null ? cur.rand.next : null; //这里加了一个不为空的判断  
        cur = next;  
    }  
  
    //最后再将新老节点分离  
    Node res = head.next;  
    cur = head;  
    while(cur != null) {  
        copyNode = cur.next;  
        cur.next = copyNode.next;  
        copyNode.next = next != null ? next.next : null;  
        cur = next;  
    }  
    return res;  

两个链表相交的问题(难)

题目:给定两个单链表,这两个链表可能有环,也可能无环。头节点head1和head2。
请实现一个函数,如果两个链表相交,返回相交的第一个节点,如果不相交,返回null.

要求:
如果两个链表长度之和为N,要求时间复杂度O(N),额外空间复杂度为O(1).

说明有环的单链表:
这道题的切入点是明确一个含有环的单链表会是怎么样的形态?
因为单链表的Node节点只有一个next指针,所以出度为1,就是说如果出现一个环,就会以该环结尾,不可能从环里面出来。![[Pasted image 20240509162820.png]]

对于两个链表的相交状态会有一下情况(根据是否有环分类)!在这里插入图片描述

所以需要一个函数来判断链表是否有环。
然后再编写两个函数分别来寻找两种情况(都有环或者都没有环)下的两个链表的相交节点

🤔❓怎么来判断一个单链表是否存在环呢?

  • 使用快慢指针遍历单链表。
  • 在遍历过程中
    ->如果可以走到null证明链表无环
    ->如果有环,那么快慢指针一定会在环中相遇。(追击问题)
  • 如果想要找到入环节点的位置:
    ->再两个指针相遇之后将快指针放到开头位置,慢指针位置不变。
    ->然后快指针改成一次走一步。
    ->最后两个节点会在环入口处相遇。

🤔❓如果两个单链表都没有环,要怎么判断两个链表是否相交?
首先明确,相交的话肯定是"Y"形态,不可能出现“X”形态。
就是说,如果相交,两个链表后面的部分是公用的,最后至少会有一个共用节点。

  • 长链表先走差值部分,短链表再开始走。
  • 最后两个链表一定会在相交节点相遇。

🤔❓如果两个单链表都有环,要怎么判断两个链表是否相交?
如果是情况二:方法和两个无环的判断是一样的
如果是情况三:就有两个交点了。返回哪个loop入口都可以

public class Code07_FindFirstIntersectNode {  
    public static class Node () {  
        public int value;  
        public Node next;  
  
        public Node(int data) {  
            this.value = data;  
        }  
    }  
  
    //主函数  
    public static Node getIntersectNode(Node head1, Node head2)  {  
        if (head1 == null || head2 == null) {  
            return null;  
        }  
        Node loop1 = getLoopNode(head1);  
        Node loop2 = getLoopNode(head2);  
        if (loop1 == null && loop2 == null) {  
            return noLoop(head1, head2);  
        }  
        if (loop1 != null && loop2 != null) {  
            return bothLoop(head1, loop1, head2, loop2);  
        }  
        //没有写但是其实对应的是一个有环一个无环的情况,这种情况是不可能相交的!  
        return null;  
    }  
  
    //返回第一个loop节点  
    public static Node getLoopNode(Node head) {  
        if (head == null || head.next == null || head.next.next == null) {  
            return null;  
        }  
        //快慢指针找环  
        Node n1 = head.next;  
        Node n2 = head.next.next;  
        //有环肯定会相遇  
        while (n1 != n2) {  
            if (n2.next == null || n2.next.next == null) {  
                return null;  
            }  
            n2 = n2.next.next;  
            n1 = n1.next;  
        }  
        //如果相遇了 快指针变慢指针放回开头  
        n2 = head;  
        while (n1 != n2) {  
            n1 = n1.next;  
            n2 = n2.next;  
        }  
        return n1;  
    }  
  
  
    //两个没有环的链表找相交节点  
    public static Node noLoop(Node head1, Node head2) {  
        if (head1 == null || head2 == null) {  
            return null;  
        }  
  
        Node cur1 = head1;  
        Node cur2 = head2;  
        int n = 0;  
        while(cur1 != null) {  
            n++;  
            cur1 = cur1.next;  
        }  
  
        while(cur2 != null) {  
            n--;  
            cur2 = cur2.next;  
        }  
  
        //如果相交 至少最后一个节点是共用的  
        if(cur1 != cur2){  
            return null;  
        }  
  
        //长链表用cur1 短链表用cur2  
        cur1 = n > 0 ? head1 : head2;  
        cur2 = cur1 == head1 ? head2 : head1;  
        n = Math.abs(n);  
        while (n != 0) {  
            //长的先走n步  
            cur1 = cur1.next;  
        }  
  
        while (cur1 != cur2) {  
            cur1 = cur1.next;  
            cur2  = cur2.next;  
        }  
        return cur1;  
    }  
  
    //两个有环的链表找相交的节点  
    public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {  
        Node cur1 = null;  
        Node cur2 = null;  
  
        if(loop1 == loop2) {  
            cur1 = head1;  
            cur2 = head2;  
            int n = 0;  
            while(cur1 != null) {  
                n++;  
                cur1 = cur1.next;  
            }  
  
            while(cur2 != null) {  
                n--;  
                cur2 = cur2.next;  
            }  
  
            //如果相交 至少最后一个节点是共用的  
            if(cur1 != cur2){  
                return null;  
            }  
  
            //长链表用cur1 短链表用cur2  
            cur1 = n > 0 ? head1 : head2;  
            cur2 = cur1 == head1 ? head2 : head1;  
            n = Math.abs(n);  
            while (n != 0) {  
                //长的先走n步  
                cur1 = cur1.next;  
            }  
  
            while (cur1 != cur2) {  
                cur1 = cur1.next;  
                cur2  = cur2.next;  
            }  
            return cur1;  
        } else {  
            cur1 = loop1.next;  
            while (cur1 !=loop1) {  
                if(cur1 == loop2) {  
                    return loop1;  
                }  
                cur1 = cur1.next;  
            }  
        }  
        return null;  
    }  
  
}
  • 33
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值