本章知识点:
- 介绍几个Java中两个常用的数据结构的 用法 +性能(不介绍原理)
- 哈希表(HashSet HashMap)
- 有序表(TreeSet TreeMap)
- 链表
Part 0 : 哈希表
在使用层面上可以理解成一种集合结构
在Java中,哈希表有( key , value ),其中key是必须的主要的,value是附带的,要不要都可以。具体来说有HashSet和HashMap两种。下面分别介绍这两种数据结构:
Part 1 : 有序表![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2bcf2a992e364fb18959e9a55db1de3e.jpeg#pic_center)
常用操作:
本章中使用的链表结构:
//单链表
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;
}
}