-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
yk
committed
Jan 12, 2024
1 parent
9b1df07
commit 6cc7766
Showing
3 changed files
with
311 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
--- | ||
title: '0104' | ||
title: '0104--排序回顾&递归时间复杂度&异或技巧' | ||
date: 2024-01-04T15:37:23+08:00 | ||
lastmod: 2024-01-08 | ||
tags: [] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,309 @@ | ||
--- | ||
title: '0109' | ||
date: 2024-01-09T20:10:19+08:00 | ||
lastmod: | ||
tags: [] | ||
series: [] | ||
categories: [] | ||
--- | ||
|
||
## 哈希表 | ||
|
||
在 javascript 中的 Set 和 Map 咱就不说啥了,非正规军 😄。来看看 java 的: | ||
|
||
1. 无序表 | ||
- HashSet: add remove contains,对呀 c++ 中的 unordered_set | ||
- HashMap: put remove containsKey,对应 c++ 中的 unordered_map | ||
2. 有序表 | ||
- TreeSet:它是按升序对元素进行排序,对应 c++ 中的 ordered_set | ||
- TreeMap:基于红黑树的 NavigableMap 实现,它根据其键的自然顺序排序,对应 c++ 中的 ordered_map | ||
|
||
记得之前学 java 的时候, HashSet 底层就是 HashMap,只不过没有 value 只有 key 罢了。 | ||
|
||
1. 放入无/有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小 | ||
2. 放入无序表的东西,如果不是基础类型,内部按引用传递,内存占用的就是这个东西的内存地址的大小,java 中是 8 个字节 | ||
3. 放入有序表的东西,如果不是基础类型,**必须提供比较器**,内部按引用传递...。 | ||
|
||
> 重点:无序表 的增删改查的时间复杂度都是 **常数** 级别(比较大的常数 哈哈),有序表的时间复杂度都是 O(logn) 级别 | ||
--- | ||
|
||
## 链表 | ||
|
||
```java | ||
class Node<V> { | ||
V value; | ||
Node next; | ||
} | ||
class Node<V> { | ||
V value; | ||
Node next; | ||
Node last; | ||
} | ||
``` | ||
|
||
> 对于链表算法,在面试中,一定尽量用到空间复杂度最小的方法(不然凭啥用咱是吧 🐶)。 | ||
### 链表换头 | ||
|
||
当链表的操作会产生换头的情况时,函数的递归调用形式应该是 `head = func(head.next)`,所以函数在设计的时候就应该有一个 Node 类型的返回值,比如反转链表,见下方。 | ||
|
||
### 链表哨兵守卫 | ||
|
||
通过在链表头部添加一个哨兵节点(dummy node),这样就无需特殊处理**头节点为空**或者**头节点变化**的边界条件。 | ||
|
||
比如链表去头加尾操作: | ||
|
||
```java | ||
/* ---------- 头部删 ---------- */ | ||
public void delHead() { | ||
ListNode p = head; | ||
/** | ||
* 没有守卫时,删除头节点的操作是 p = p.next | ||
* if (p != null) { | ||
* p = p.next; | ||
* } | ||
* 而删除其他的节点的操作是 p.next = p.next.next, | ||
* 所以就需要分别讨论删除head和其它节点的情况 | ||
* | ||
* 恰恰因为有 dummy 守卫节点的存在,无论头节点还是其他节点都统一使用 p.next = p.next.next | ||
* if (p.next != null) { | ||
* p.next = p.next.next; | ||
* } | ||
* 就简化了边界条件的判断 | ||
*/ | ||
} | ||
``` | ||
|
||
再比如,需要创建一条新链表,见下方分离链表的操作。 | ||
|
||
<!-- public void deleteAtIndex(int index) { | ||
ListNode p = head; | ||
for (int i = 0; i < index; i++) { | ||
p = p.next; | ||
if (p == null) { | ||
return; // 超出链表长度 | ||
} | ||
} | ||
if (p.next != null) { | ||
p.next = p.next.next; | ||
} | ||
} | ||
public void deleteAtIndex(int index) { | ||
if (index < 0 || head == null) { | ||
return; | ||
} | ||
if (index == 0) { | ||
head = head.next; | ||
return; | ||
} | ||
ListNode p = head; | ||
for (int i = 0; i < index - 1; i++) { | ||
p = p.next; | ||
if (p == null) { | ||
return; // 超出链表长度 | ||
} | ||
} | ||
if (p.next != null) { | ||
p.next = p.next.next; | ||
} | ||
} --> | ||
|
||
```java | ||
/* ---------- 尾部增 ---------- */ | ||
public void addTrail(ListNode node) { | ||
/** | ||
* 当 head 为 null 时,在尾部添加node就需要处理head为null的边界条件 | ||
* if (head == null) { | ||
* head = node; | ||
* return; | ||
* } | ||
*/ | ||
// 当 head = dummy (new ListNode(-1)) 时,就不需要上面的边界条件处理了 | ||
ListNode p = head; | ||
while (p.next != null) { | ||
p = p.next; | ||
} | ||
p.next = node; | ||
} | ||
``` | ||
|
||
### 链表中常用的技巧-快慢指针 | ||
|
||
#### 找到链表的中点、中点前一个、中点后一个 | ||
|
||
这个是硬编码能力,需要大量练习打好基本功。 | ||
|
||
```java | ||
/** | ||
* 奇数的中点 & 偶数的中点靠前一位 | ||
*/ | ||
Node s = head; | ||
Node f = head; | ||
while (f.next != null && f.next.next != null) { | ||
s = s.next; | ||
f = f.next.next; | ||
} | ||
|
||
/** | ||
* 奇数的中点 & 偶数的中点靠后一位 | ||
*/ | ||
while (f != null && f.next != null) { | ||
s = s.next; | ||
f = f.next.next; | ||
} | ||
``` | ||
|
||
如果要进一步继续偏移,修改 f 或 s 的初始节点即可。 | ||
|
||
注意:因为 f 一次走两步,所以: | ||
|
||
- 想要获取中点往前的节点,修改 f 初始节点时 f=head.next.next,两个 next 才会让结果往前偏移一步 | ||
- 想要获取中点往后的节点,修改 s 的初始节点 s=head.next,一个 next 就可以让结果往后偏移一步 | ||
|
||
### 练习 | ||
|
||
- 反转链表(recursion&iteration 两种方式)(lc.206) | ||
|
||
```java | ||
// 迭代法,那就是双指针了,先存储后继节点,剩下的都好办 | ||
public ListNode reverseList(ListNode head) { | ||
ListNode p = head, pre = null; | ||
while(p != null) { | ||
ListNode next = p.next; // 储存后继节点 | ||
p.next = pre; | ||
pre = p; | ||
p = next; | ||
} | ||
return pre; | ||
} | ||
// 递归法 | ||
public ListNode reverseList(ListNode head) { | ||
if (head == null || head.next == null) return head; | ||
ListNode newHead = reverseList(head.next); // newHead 是最后一个节点 | ||
// 后序遍历,假设递归到最后了,此时的 head 是最后一个节点的前一个节点 | ||
head.next.next = head; // 把下一个节点的next指向自身 | ||
head.next = null; // 把自身的next指向null | ||
return newHead; // 返回尾部节点的引用指针即可 | ||
} | ||
``` | ||
|
||
- 判断一个链表是否是回文结构(lc.234) | ||
|
||
```java | ||
/** | ||
* 这道题解法很多,如果借助一个额外的栈就很简单, | ||
* 但是面试中链表一定要找到额外空间复杂度最佳的方法。 | ||
* 要求时间复杂度 O(n), 空间复杂度 O(1) | ||
*/ | ||
class Solution { | ||
public boolean isPalindrome(ListNode head) { | ||
// 要想空间复杂度为 O(1),把后半部分链表反转过来,然后比较即可 | ||
ListNode s = head, f = head; | ||
while(f != null && f.next != null) { | ||
s = s.next; | ||
f = f.next.next; | ||
} | ||
// 此时 s 在中点或者中点的下一个节点,对后半部分反转链表(这里是另一种双指针反转方式) | ||
ListNode curr = s; | ||
while(s.next != null) { | ||
// 缓存下下个节点,最后反转完了后,head恰好指向原来的尾 null 上 | ||
ListNode next = s.next.next; | ||
s.next.next = curr; // 下一个节点指向当前节点 | ||
curr = s.next; // 当前节点指针移动到下个节点 | ||
s.next = next; // s节点指针指向下下个节点 | ||
} | ||
|
||
boolean isPalindrome = true; | ||
s = head; | ||
while(curr != null && s != null) { | ||
if(curr.val != s.val) { | ||
return false; | ||
} | ||
curr = curr.next; | ||
s = s.next; | ||
} | ||
/** | ||
* 最后应该把反转的链表再反转回去[旺柴] | ||
*/ | ||
return true; | ||
} | ||
} | ||
``` | ||
|
||
- 将链表划分成左边小、中间相等、右边大的形式 | ||
```java | ||
/** | ||
* 这题如果借助数组,那不就是快排的 partition 吗? | ||
* | ||
* 要想时间复杂度为 O(1),那么就得借助 6 个变量 <head <tail =head =tail >head >tail | ||
* 需要注意的点就是,要判断变量是否为 null | ||
*/ | ||
``` | ||
- 复制含有随机指针的链表 (lc.138) | ||
|
||
```java | ||
/** | ||
* 这道题要是空间复杂度不需要 O(1),那么用哈希表就挺好做的 | ||
* | ||
* O(1) 的空间复杂度,就需要一定的技巧了,就是 拼接+拆分。 | ||
*/ | ||
// 哈希表 | ||
class Solution { | ||
public Node copyRandomList(Node head) { | ||
Map<Node, Node> map = new HashMap<>(); | ||
Node p = head; | ||
while (p != null) { | ||
map.put(p, new Node(p.val)); | ||
p = p.next; | ||
} | ||
p = head; | ||
while(p != null) { | ||
map.get(p).next = map.get(p.next); | ||
map.get(p).random = map.get(p.random); | ||
p = p.next; | ||
} | ||
return map.get(head); | ||
} | ||
} | ||
``` | ||
|
||
```java | ||
public Node copyRandomList(Node head) { | ||
// 恰到好处的拼接,复制节点直接拼到原节点后,这样会发现: | ||
// ** 新节点的random指向的就是原节点random指向节点的下一个节点 ** | ||
// 1.复制节点 | ||
Node p = head; | ||
while (p != null) { | ||
Node newNode = new Node(p.val); | ||
newNode.next = p.next; | ||
p.next = newNode; | ||
p = newNode.next; | ||
} | ||
// 2.设置random | ||
p = head; | ||
while (p != null) { | ||
if(p.random != null) { | ||
p.next.random = p.random.next; | ||
} | ||
p = p.next.next; | ||
} | ||
// 3.分离链表 | ||
Node dummy = new Node(-1); | ||
p = head; | ||
Node curr = dummy; | ||
while (p != null) { | ||
curr.next = p.next; | ||
curr = curr.next; | ||
p.next = curr.next; | ||
p = p.next; | ||
} | ||
return dummy.next; | ||
} | ||
``` | ||
|
||
- 两个单链表相交系列问题 | ||
- 1 | ||
- 2 | ||
- 3 |