From 6cc7766085c5d03773f2c6eb4cabcd82305b4b0b Mon Sep 17 00:00:00 2001 From: yk Date: Tue, 9 Jan 2024 20:40:36 +0800 Subject: [PATCH] 0109 --- config.toml | 2 +- content/posts/2024/01/0104.md | 2 +- content/posts/2024/01/0109.md | 309 ++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 content/posts/2024/01/0109.md diff --git a/config.toml b/config.toml index 62f8b35..2529727 100644 --- a/config.toml +++ b/config.toml @@ -351,7 +351,7 @@ auto = false # 是否显示代码块的复制按钮 copy = true # 默认展开显示的代码行数 -maxShownLines = 30 +maxShownLines = 50 # 表格配置 # [params.page.table] # # 是否开启表格排序 diff --git a/content/posts/2024/01/0104.md b/content/posts/2024/01/0104.md index 6ea2226..2a78ad7 100644 --- a/content/posts/2024/01/0104.md +++ b/content/posts/2024/01/0104.md @@ -1,5 +1,5 @@ --- -title: '0104' +title: '0104--排序回顾&递归时间复杂度&异或技巧' date: 2024-01-04T15:37:23+08:00 lastmod: 2024-01-08 tags: [] diff --git a/content/posts/2024/01/0109.md b/content/posts/2024/01/0109.md new file mode 100644 index 0000000..525a9bd --- /dev/null +++ b/content/posts/2024/01/0109.md @@ -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 value; + Node next; +} +class Node { + 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; + * } + * 就简化了边界条件的判断 + */ +} +``` + +再比如,需要创建一条新链表,见下方分离链表的操作。 + + + +```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 + * 需要注意的点就是,要判断变量是否为 null + */ + ``` +- 复制含有随机指针的链表 (lc.138) + + ```java + /** + * 这道题要是空间复杂度不需要 O(1),那么用哈希表就挺好做的 + * + * O(1) 的空间复杂度,就需要一定的技巧了,就是 拼接+拆分。 + */ + // 哈希表 + class Solution { + public Node copyRandomList(Node head) { + Map 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