# 链表
# 哈希表的简单介绍
- 哈希表在使用层面上可以理解为一种集合结构。
- 如果只有 key,没有伴随数据 value,可以使用 HashSet 结构 (C++ 中叫 UnOrderedSet)。
- 如果既有 key,又有伴随数据 value,可以使用 HashMap 结构 (C++ 中叫 UnOrderedMap)。
- 有无伴随数据,是 HashMap 和 HashSet 唯一的区别,底层的实际结构是一回事。
- 使用哈希表 (put)、删 (remove)、改 (put)、查 (get) 的操作,可以认为时间复杂度为 O (1),但是常数时间比较大。
- 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小。
- 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小 (8bit)。
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 链表节点 | |
* @DateTime: 2023-05-09 21:38 | |
* @Version:1.0 | |
**/ | |
public class Node { | |
private int data; | |
private Node next; | |
public Node() { | |
} | |
public Node(int data, Node next) { | |
this.data = data; | |
this.next = next; | |
} | |
public Node(int i) { | |
this.data = i; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o == null || getClass() != o.getClass()) { | |
return false; | |
} | |
Node node = (Node) o; | |
return data == node.data && Objects.equals(next, node.next); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(data, next); | |
} | |
} |
import java.util.HashMap; | |
import java.util.HashSet; | |
/** | |
* @Author: LightRain | |
* @Description: HashSet 和 HashMap 的使用 | |
* 哈希表在使用的时候都是常数级别的时间复杂度 | |
* 和数据量没有关系,数据量再大也可以做到常数 | |
* 级别的返回值 | |
* 时间复杂度:O (logN) | |
* @DateTime: 2023-05-09 21:37 | |
* @Version:1.0 | |
**/ | |
public class HashSetAndHashMap { | |
public static void main(String[] args) { | |
Node nodeA = null; | |
Node nodeB = null; | |
Node nodeC = null; | |
HashSet<Integer> hashSet1 = new HashSet<>(); | |
hashSet1.add(3); | |
System.out.println("hashSet1.contains(3) = " + hashSet1.contains(3)); | |
hashSet1.remove(3); | |
System.out.println("hashSet1.contains(3) = " + hashSet1.contains(3)); | |
System.out.println("================================================"); | |
HashMap<Integer, String> hashMap = new HashMap<>(); | |
hashMap.put(1, "zuo"); | |
hashMap.put(1, "cheng"); | |
hashMap.put(2, "2"); | |
System.out.println("hashMap.containsKey(1) = " + hashMap.containsKey(1)); | |
System.out.println("hashMap.get(1) = " + hashMap.get(1)); | |
System.out.println("hashMap.get(4) = " + hashMap.get(4)); | |
hashMap.remove(2); | |
System.out.println("hashMap.get(2) = " + hashMap.get(2)); | |
System.out.println("================================================"); | |
//HashSet2 的 key 是非基础类型 ->Node 类型 | |
nodeA = new Node(1); | |
nodeB = new Node(1); | |
HashSet<Node> hashSet2 = new HashSet<>(); | |
hashSet2.add(nodeA); | |
System.out.println("hashSet2.contains(nodeA) = " + hashSet2.contains(nodeA)); | |
System.out.println("hashSet2.contains(nodeB) = " + hashSet2.contains(nodeB)); | |
hashSet2.remove(nodeA); | |
System.out.println("hashSet2.contains(nodeA) = " + hashSet2.contains(nodeA)); | |
System.out.println("================================================"); | |
//HashMap1 的 key 是基础类型 ->String 类型 | |
HashMap<String, Integer> hashMap1 = new HashMap<>(); | |
String str1 = "key"; | |
String str2 = "key"; | |
hashMap1.put(str1, 1); | |
System.out.println("hashMap1.containsKey(str1) = " + hashMap1.containsKey(str1)); | |
System.out.println("hashMap1.containsKey(str2) = " + hashMap1.containsKey(str2)); | |
System.out.println("hashMap1.get(str1) = " + hashMap1.get(str1)); | |
System.out.println("hashMap1.get(str2) = " + hashMap1.get(str2)); | |
hashMap1.put(str2, 2); | |
System.out.println("hashMap1.containsKey(str1) = " + hashMap1.containsKey(str1)); | |
System.out.println("hashMap1.containsKey(str2) = " + hashMap1.containsKey(str2)); | |
System.out.println("hashMap1.get(str1) = " + hashMap1.get(str1)); | |
System.out.println("hashMap1.get(str2) = " + hashMap1.get(str2)); | |
hashMap1.remove(str1); | |
System.out.println("hashMap1.containsKey(str1) = " + hashMap1.containsKey(str1)); | |
System.out.println("hashMap1.containsKey(str2) = " + hashMap1.containsKey(str2)); | |
System.out.println("================================================"); | |
} | |
} |
# 有序表的简单介绍
- 有序表在使用层面上可以理解为一种集合结构。
- 如果只有 key,没有伴随数据 value,可以使用 TreeSet 结构 (C++ 中叫 OrderedSet)。
- 如果既有 key,又有伴随数据 value,可以使用 TreeMap 结构 (C++ 中叫 OrderedMap)。
- 有无伴随数据,是 TreeSet 和 TreeMap 唯一的区别,底层的实际结构是一回事。
- 有序表和哈希表的区别是,有序表把 key 按照顺序组织起来,而哈希表完全不组织。
- 红黑树、AVL 数、size-balance-tree 和跳表等都属于有序表结构,只是底层具体实现不同。
- 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小。
- 放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小。
- 不管是什么底层具体实现,只要是有序表,都有以下固定的基本功能和固定的时间复杂度 O (logN) 级别。
# 有序表的固定操作
- void put (K key,V value): 将一个 (key,value) 记录加入到表中,或者将 key 的记录更新成 value。
- V get (K key): 根据给定的 key,查询 value 并返回。
- void remove (K key): 移除 key 的记录。
- boolean containsKey (K key): 询问是否有关于 key 的记录。
- K firstKey (): 返回所有键值的排序结果中,最左 (最小) 的那个。
- K lastKey (): 返回所有键值的排序结果中,最右 (最大) 的那个。
- K floorKey (K key): 如果表中存入过 key,返回 key: 否则返回所有键值的排序结果中,key 的前一个。
- K ceilingKey (K key): 如果表中存入过 key,返回 key: 否则返回所有键值的排序结果中,key 的后一个。
- 以上所有操作时间复杂度都是 O (logN),N 为有序表含有的记录数。
import java.util.Comparator; | |
import java.util.Objects; | |
/** | |
* @Author: LightRain | |
* @Description: 链表节点 | |
* @DateTime: 2023-05-09 21:38 | |
* @Version:1.0 | |
**/ | |
public class Node { | |
private int data; | |
private Node next; | |
public Node() { | |
} | |
public Node(int data, Node next) { | |
this.data = data; | |
this.next = next; | |
} | |
public Node(int i) { | |
this.data = i; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (o == null || getClass() != o.getClass()) { | |
return false; | |
} | |
Node node = (Node) o; | |
return data == node.data && Objects.equals(next, node.next); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(data, next); | |
} | |
/** | |
* 自定义比较器 | |
*/ | |
public static class NodeComparator implements Comparator<Node> { | |
@Override | |
public int compare(Node o1, Node o2) { | |
return o1.data - o2.data; | |
} | |
} | |
} |
import java.util.TreeMap; | |
import java.util.TreeSet; | |
/** | |
* @Author: LightRain | |
* @Description: TreeSet 和 TreeMap | |
* 有序表在使用的时候都是常数级别的时间复杂度 | |
* 和数据量没有关系,数据量再大也可以做到常数 | |
* 级别的返回值 | |
* 时间复杂度:O (logN) | |
* @DateTime: 2023-05-09 22:28 | |
* @Version:1.0 | |
**/ | |
public class TreeSetAndTreeMap { | |
public static void main(String[] args) { | |
Node nodeA = null; | |
Node nodeB = null; | |
Node nodeC = null; | |
nodeA = new Node(5); | |
nodeB = new Node(3); | |
nodeC = new Node(7); | |
// 红黑树 | |
TreeSet<Node> treeSet = new TreeSet<>(); | |
// 以下代码会报错,因为没有提供 Node 类型的比较器 | |
try { | |
treeSet.add(nodeA); | |
treeSet.add(nodeB); | |
treeSet.add(nodeC); | |
} catch (Exception e) { | |
System.out.println("错误信息:" + e.getMessage()); | |
} | |
System.out.println("================================================"); | |
// 添加比较器才可添加节点成功 | |
treeSet = new TreeSet<>(new Node.NodeComparator()); | |
try { | |
treeSet.add(nodeA); | |
treeSet.add(nodeB); | |
treeSet.add(nodeC); | |
System.out.println("节点添加成功"); | |
} catch (Exception e) { | |
System.out.println("错误信息:" + e.getMessage()); | |
} | |
System.out.println("================================================"); | |
// 展示有序表常用操作 | |
TreeMap<Integer, String> treeMap1 = new TreeMap<>(); | |
treeMap1.put(7, "我是7"); | |
treeMap1.put(5, "我是5"); | |
treeMap1.put(4, "我是4"); | |
treeMap1.put(3, "我是3"); | |
treeMap1.put(9, "我是9"); | |
treeMap1.put(2, "我是2"); | |
System.out.println(treeMap1.containsKey(5)); | |
System.out.println(treeMap1.get(5)); | |
System.out.println(treeMap1.firstKey() + ",我最小"); | |
System.out.println(treeMap1.lastKey() + ",我最大"); | |
System.out.println(treeMap1.floorKey(8) + ",在表中所有<=8的数中,我离8最近"); | |
System.out.println(treeMap1.ceilingKey(8) + ",在表中所有>=8的数中,我离8最近"); | |
System.out.println(treeMap1.floorKey(7) + ",在表中所有<=7的数中,我离7最近"); | |
System.out.println(treeMap1.ceilingKey(7) + ",在表中所有>=7的数中,我离7最近"); | |
treeMap1.remove(5); | |
System.out.println(treeMap1.get(5) + ",删了就没有了哦"); | |
System.out.println("================================================"); | |
} | |
} |
# 单链表的节点结构
class Node<V> { | |
V value; | |
Node next; | |
} |
由以上结构的节点依次连接起来所形成的链叫单链表结构。
# 双链表的节点结构
class Node<V> { | |
V value; | |
Node next; | |
Node last; | |
} |
由以上结构的节点依次连接起来所形成的链叫双链表结构。
单链表和双链表结构只需要给定一个头部节点 head,就可以找到剩下的所有节点。
# 反转单向和双向链表
- 题目:分别实现反转单向链表和反转双向链表的函数。
- 要求:如果链表长度为 N, 时间复杂度要求为 O (N), 额外空间复杂度要求为 O (1)。
- 代码以后再贴。
# 打印两个有序表的公共部分
- 题目:给定两个有序链表的头指针 head1 和 head2, 打印两个链表的公共部分。
- 要求:如果两个链表的长度之和为 N, 时间复杂度要求为 O (N), 额外空间复杂度要求为 O (1)。
- 代码以后再贴。
# 面试时链表解题的方法论
- 对于笔试,不用太在乎空间复杂度,一切为了时间复杂度。
- 对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法。
# 重要技巧
- 额外数据结构记录 (哈希表等)。
- 快慢指针。
# 判断一个链表是否为回文结构
- 题目:给定一个单链表的头节点 head, 请判断该链表是否为回文结构。
- 例子:1->2->1, 返回 true;1->2->2->1, 返回 true;15->6->15, 返回 true;1->2->3, 返回 false。
- 例子:如果链表长度为 N,时间复杂度达到 O (N),额外空间复杂度达到 O (1)。
import java.util.Stack; | |
/** | |
* @Author: LightRain | |
* @Description: 判断一个链表是否为回文结构 | |
* @DateTime: 2023-05-09 23:40 | |
* @Version:1.0 | |
**/ | |
public class Palindrome { | |
public static void main(String[] args) { | |
// 测试不会写之后再补 | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/9 23:41 | |
* @Param: [head] | |
* @Return: boolean | |
* @Description: need n extra space 需要 n 个额外的空间,压入到栈形式的回文 | |
* @since 17 | |
*/ | |
public static boolean isPalindrome1(Node head) { | |
// 创建 Stack 对象 | |
Stack<Node> stack = new Stack<>(); | |
Node cur = head; | |
while (cur != null) { | |
// 将所有东西压入进栈内 | |
stack.push(cur); | |
cur = cur.next; | |
} | |
// 不为空则继续遍历 | |
while (head != null) { | |
// 栈弹出一个对比一个 全部都一样返回 true | |
if (head.value != stack.pop().value) { | |
return false; | |
} | |
head = head.next; | |
} | |
return true; | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/9 23:53 | |
* @Param: [head] | |
* @Return: boolean | |
* @Description: need n/2 extra space 需要 n2 额外空间,二分之 N 方法查找快慢指针 | |
* @since 17 | |
*/ | |
public static boolean isPalindrome2(Node head) { | |
if (head == null || head.next == null) { | |
return true; | |
} | |
Node right = head.next; | |
Node cur = head; | |
while (cur.next != null && cur.next.next != null) { | |
right = right.next; | |
cur = cur.next.next; | |
} | |
Stack<Node> stack = new Stack<>(); | |
while (right != null) { | |
stack.push(right); | |
right = right.next; | |
} | |
while (!stack.isEmpty()) { | |
if (head.value != stack.pop().value) { | |
return false; | |
} | |
head = head.next; | |
} | |
return true; | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/10 0:33 | |
* @Param: [head] | |
* @Return: boolean | |
* @Description: need O (1) extra space 需要 O (1) 个额外空间实现判断一个链表是否为回文结构 | |
* @since 17 | |
*/ | |
public static boolean isPalindrome3(Node head) { | |
if (head == null || head.next == null) { | |
return true; | |
} | |
Node n1 = head; | |
Node n2 = head; | |
//find mid node 查找中间节点 | |
//n2 走完 n1 会来到中点位置 | |
while (n2.next != null && n2.next.next != null) { | |
//n1 一次走一步 n1 -> mid | |
n1 = n1.next; | |
//n2 一次走两步 n2 -> end | |
n2 = n2.next.next; | |
} | |
//n2 -> right part first node 右部分第一节点 | |
n2 = n1.next; | |
//mid.next -> null | |
n1.next = null; | |
Node n3 = null; | |
//right part convert | |
// 从 n1 开始逆序 | |
while (n2 != null) { | |
// 右半部分开始逆序 | |
//n3 -> save next node 保存下一个节点 | |
n3 = n2.next; | |
//next of right node convert 右节点转换的下一个 | |
n2.next = n1; | |
//n1 move n1 移动 | |
n1 = n2; | |
//n2 move n2 移动 | |
n2 = n3; | |
} | |
// 右边逆序到中点时 | |
//n3 -> save last node 保存最后一个节点 | |
n3 = n1; | |
//n2 -> left first node 左第一节点 | |
n2 = head; | |
boolean res = true; | |
//check palindrome 检查回文 | |
// 左边一个点 | |
// 右边一个点 | |
while (n1 != null && n2 != null) { | |
//n1 和 n2 每从一步验证一次,不一致 res 设置为 false | |
if (n1.value != n2.value) { | |
res = false; | |
break; | |
} | |
//left to mid 左至中 | |
//n1 从中间走 | |
n1 = n1.next; | |
//right to mid 从右边到中间 | |
//n2 也从中间走 | |
n2 = n2.next; | |
} | |
n1 = n3.next; | |
n3.next = null; | |
//recover list 恢复右半逆序链表 | |
while (n1 != null) { | |
n2 = n1.next; | |
n1.next = n3; | |
n3 = n1; | |
n1 = n2; | |
} | |
return res; | |
} | |
/** | |
* 链表节点 | |
*/ | |
private static class Node { | |
public int value; | |
public Node next; | |
public Node(int value) { | |
this.value = value; | |
} | |
} | |
} |
# 将单向链表按照某值划分
- 将单向链表按照某值划分成左边小、中间相等、右边大的形式。
- 题目:给定一个单链表的头节点 head,节点的值类型是整形,再给定一个整数 pivot。
- 实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot 的节点,中间部分都是值等于 pivot 的节点,右部分都是值大于 pivot 的节点。
【进阶】在实现原问题功能的基础上增加如下的要求
- 调整后所有小于 pivot 的节点之间的相对顺序和调整前一样。
- 调整后所有等于 pivot 的节点之间的相对顺序和调整前一样。
- 调整后所有大于 pivot 的节点之间的相对顺序和调整前一样。
- 时间复杂度请达到 O (N),额外空间复杂度请达到 O (1)。
/** | |
* @Author: LightRain | |
* @Description: 将单向链表按照某值划分成左边小、中间相等、右边大的形式。 | |
* @DateTime: 2023-05-10 00:51 | |
* @Version:1.0 | |
**/ | |
public class SmallerEqualBigger { | |
public static void main(String[] args) { | |
// 测试不会写之后再补 | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/10 1:05 | |
* @Param: [head, pivot] | |
* @Return: com.example.cleanemptyfolders.sort.SmallerEqualBigger.Node | |
* @Description: 列表分区 | |
* @since 17 | |
*/ | |
public static Node listPartition(Node head, int pivot) { | |
//small head 小于头 | |
Node sH = null; | |
//small tail 小于尾 | |
Node sT = null; | |
//equal head 等于头 | |
Node eH = null; | |
//equal tail 等于尾 | |
Node eT = null; | |
//big head 大于头 | |
Node mH = null; | |
//big tail 大于尾 | |
Node mT = null; | |
//save next node 保存下一个节点 | |
Node next = null; | |
//every node distributed to three lists | |
// 每个节点分发到三个列表 | |
while (head != null) { | |
next = head.next; | |
head.next = null; | |
if (head.value < pivot) { | |
// 第一个节点 | |
if (sH == null) { | |
sH = head; | |
sT = head; | |
} else { | |
// 如果不是第一个节点将老的尾巴链接到当前 head 节点 | |
sT.next = head; | |
// 当前节点变成小于区的新的尾巴 | |
sT = head; | |
} | |
} else if (head.value == pivot) { | |
if (eH == null) { | |
eH = head; | |
eT = head; | |
} else { | |
eT.next = head; | |
eT = head; | |
} | |
} else { | |
if (mH == null) { | |
mH = head; | |
mT = head; | |
} else { | |
mT.next = head; | |
mT = head; | |
} | |
} | |
head = next; | |
} | |
//small and equal reconnect | |
// 小而相等的重新连接 | |
if (sT != null) { | |
sT.next = eH; | |
// 下一步,谁去链接大于区域的头谁就变成 eT | |
eT = eT == null ? sT : eT; | |
} | |
// 上面的 if, 不管跑了没有 eT | |
//all reconnect 全部重新连接 | |
if (eT != null) { | |
// 如果小于区域和等于区域,不是都没有 | |
eT.next = mH; | |
} | |
return sH != null ? sH : (eH != null ? eH : mH); | |
} | |
/** | |
* 链表节点 | |
*/ | |
private static class Node { | |
public int value; | |
public Node next; | |
public Node(int value) { | |
this.value = value; | |
} | |
} | |
} |
# 复制含有随机指针节点的链表
- 题目一种特殊的单链表节点类描述如下
class Node{
int value;
Node next;
Node rand;
Node (int val){
value = val;
}
}
- rand 指针是单链表节点结构中新增的指针,rand 可能指向链表中的任意一个节点,也可能指向 null。
- 给定一个由 Node 节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度 O (N),额外空间复杂度 O (1)
import java.util.HashMap; | |
/** | |
* @Author: LightRain | |
* @Description: 复制含有随机指针节点的链表 | |
* @DateTime: 2023-05-10 01:47 | |
* @Version:1.0 | |
**/ | |
public class CopyListWithRand { | |
public static void main(String[] args) { | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/10 2:07 | |
* @Param: [head] | |
* @Return: com.example.cleanemptyfolders.sort.CopyListWithRand.Node | |
* @Description: 使用链表完成节点复制 | |
* @since 17 | |
*/ | |
public static Node copyListWithRand1(Node head) { | |
// 创建 Map | |
HashMap<Node, Node> map = new HashMap<>(); | |
Node cur = head; | |
while (cur != null) { | |
// 做出克隆节点添加到 map 内 | |
map.put(cur, new Node(cur.value)); | |
// 进行下一个节点 | |
cur = cur.next; | |
} | |
cur = head; | |
while (cur != null) { | |
//cur 老链表 | |
//map.get (cur) 新链表 | |
map.get(cur).next = map.get(cur.next); | |
map.get(cur).rand = map.get(cur.rand); | |
// 进行下一个节点 | |
cur = cur.next; | |
} | |
return map.get(head); | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/10 2:07 | |
* @Param: [head] | |
* @Return: com.example.cleanemptyfolders.sort.CopyListWithRand.Node | |
* @Description: 不使用链表完成节点复制 | |
* @since 17 | |
*/ | |
public static Node copyListWithRand2(Node head) { | |
if (head == null) { | |
return null; | |
} | |
Node cur = head; | |
Node next = null; | |
//copy node and link to every node 复制节点并链接到每个节点 | |
// 1->2->3 | |
// 1->1'->2->2'->3->3' | |
while (cur != null) { | |
next = cur.next; | |
// 当前节点的下一个放克隆节点 | |
cur.next = new Node(cur.value); | |
// 克隆节点的下一个是老的下一个节点 | |
cur.next.next = next; | |
cur = next; | |
} | |
cur = head; | |
Node curCopy = null; | |
//set copy node rand 设置复制节点 rand | |
//1->1'->2->2' | |
while (cur != null) { | |
next = cur.next.next; | |
curCopy = cur.next; | |
// 如果 cur.rand != null 设置为 cur.rand.next 否则设置为 null | |
curCopy.rand = cur.rand != null ? cur.rand.next : null; | |
cur = next; | |
} | |
Node res = head.next; | |
cur = head; | |
//split 拆分 | |
while (cur != null) { | |
next = cur.next.next; | |
curCopy = cur.next; | |
cur.next = next; | |
curCopy.next = next != null ? next.next : null; | |
cur = next; | |
} | |
return res; | |
} | |
/** | |
* 链表节点 | |
*/ | |
private static class Node { | |
public int value; | |
public Node next; | |
public Node rand; | |
public Node(int value) { | |
this.value = value; | |
} | |
} | |
} |