diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..38ce3a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Mobile Tools for Java (J2ME) +.mtj.tmp/ +out + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# eclipse ignore +.settings +.project +.classpath +.tomcatplugin +logPath_IS_UNDEFINED +*.gz +disconf +dist +logs + +# idea ignore +.idea +*.iml + +# maven ignore +target + +# other ignore +*.log +*.tmp +Thumbs.db +*.DS_Store +.factorypath diff --git a/README.md b/README.md index 4d40aca4..828e69d2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,43 @@ -# 极客大学「算法训练营-第24期」作业提交仓库 +极客大学「算法训练营-第24期」学习总结 +--- +## 学习内容 +- 知识内容 + * 数组 + * 链表 + * 跳表 + * 栈 + * 队列 + * 哈希、映射、集合 + * 树、二叉树、二叉搜索树 + * 堆和堆排序 + * 图 + * 递归 + * 分治 + * 回溯 + * 深度+广度优先遍历 + * 贪心算法 + * 二分查找 + * 动态规划 + * Trie树 + * 红黑树 + * AVL树 + * 位运算 + * 布隆过滤器 + * LRU缓存 + * 排序算法 + * 字符串算法 +- 五毒神掌,五遍刷题法,强调练习算法题要过遍数,算法题不仅是做一遍通过而已,而是要通过刻意练习达到对算法熟练的效果。 + * 五分钟没思路,直接看优秀题解 + * 第一遍可以参考优秀答案 + * 第二遍自己默写,比较国内优秀解法 + * 第三遍看国际站,比较国际优秀解法 + * 第四遍默写+总结,记录优秀的解法 + * 第五遍用时前快速熟悉 +- 四步解题法 + * clarification 沟通题目/审题/边界条件/输入输出 + * possible solutions --> 所有的解法都思考一遍,optimal找到一种最优的解法(时间空间复杂度分析,自顶向下+边界+图解) + * code 确认后,编码 + * test cases 测试用例 + - -## 仓库目录结构说明 - -1. `week01/` 代表第一周作业提交目录,以此类推。 -2. 请在对应周的目录下新建或修改自己的代码作业。 -2. 每周均有一个 `README.md` 文档,你可以将自己当周的学习心得以及做题过程中的思考记录在该文档中。 - -## 作业提交规则 - -1. 先将本仓库 Fork 到自己 GitHub 账号下。 -2. 将 Fork 后的仓库 Clone 到本地,然后在本地仓库中对应周的目录下新建或修改自己的代码作业,当周的学习总结写在对应周的README.md文件里。 -3. 在本地仓库完成作业后,push 到自己的 GitHub 远程仓库。 -4. 最后将远程仓库中当周的作业链接,按格式贴到班级仓库对应学习周的issue下面。 -5. 提交issue请务必按照规定格式进行提交,否则作业统计工具将抓取不到你的作业提交记录。 - -详细的作业提交流程可以查阅:https://shimo.im/docs/m5rtM8K8rNsjw5jk/ - - -## 注意事项 - - 如果对 Git 和 GitHub 不太了解,请参考 [Git 官方文档](https://git-scm.com/book/zh/v2) 或者极客时间的[《玩转 Git 三剑客》](https://time.geekbang.org/course/intro/145)视频课程。 diff --git a/Week_01/README.md b/Week_01/README.md index 50de3041..927e1c29 100644 --- a/Week_01/README.md +++ b/Week_01/README.md @@ -1 +1,59 @@ -学习笔记 \ No newline at end of file +本周学习了数组、链表、跳表、栈、队列五种数据结构,做了部分练习,整体还没进入节奏,投入时间较少。 + +--- + +### 一、学习笔记 +- [数组](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/note/array.md) +- [链表](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/note/list.md) +- [跳表](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/note/skiplist.md) +- [栈](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/note/stack.md) +- [队列](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/note/queue.md) + + +### 二、本周作业 +简单: +- [用 add first 或 add last 这套新的 API 改写 Deque 的代码](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework) +- [分析 Queue 和 Priority Queue 的源码](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework) +- [删除排序数组中的重复项(Facebook、字节跳动、微软在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_016_26_remove_duplicates_from_sorted_array) +- [旋转数组(微软、亚马逊、PayPal 在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_017_189_rotate_array) +- [合并两个有序链表(亚马逊、字节跳动在半年内面试常考)](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework/MergeTwoLists.java) +- [合并两个有序数组(Facebook 在半年内面试常考)](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework/MergeTwoArrays.java) +- [两数之和(亚马逊、字节跳动、谷歌、Facebook、苹果、微软在半年内面试中高频常考)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_003_1_two_sum) +- [移动零(Facebook、亚马逊、苹果在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_005_283_move_zeros) +- [加一(谷歌、字节跳动、Facebook 在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_002_66_plus_one) + +中等: +- [设计循环双端队列(Facebook 在 1 年内面试中考过)] + +困难: +- [接雨水(亚马逊、字节跳动、高盛集团、Facebook 在半年内面试常考)] + +### 三、训练场练习 +- 数组相关: + * [选择餐馆] +- 链表相关: + * [合并两个有序链表] +- 栈相关: + * [视野总和] + * [每日在线用户量] +- 队列相关: + * [数据流查询] + + +### 四、心得及小结 +心得 +- 五毒神掌5分钟限时贯彻不彻底,还是会和题目杠上,最后又是一地鸡毛 +- 五毒神掌重复刷题贯彻不彻底,目前还是一遍的居多,需要按艾宾浩斯记忆法第0天/+1天/+3天/+7天/+30天 +- 第一遍参考别人的代码,有时候直接抄过来,其实没有理解,抄完了其实没有做完,需要理解清楚 + +本周小结 +- Arrays.asList()不能直接作用于基本数据类型如int,asList接收的是一个泛型变长参数,而我们知道基本类型是不能泛型化的,就是说8种基本类型不能作为泛型参数,要想作为泛型参数就要使用其所对应的包装类。 +- 源码实现种单链表实际用的比较少,一般都是使用双端链表 + +### 五、疑问及困惑 + +- 对于链表的指针没有理解透彻,时长犯晕 pre = pre.next、pre.next = pre.next.next? +- 很多题都有很多种解法,对于参考来说,我们需要看几种解法就够了? + +### 六、练习汇总 +[leetcode-做题记录](https://github.com/xiaoboji/j-leetcode) \ No newline at end of file diff --git a/Week_01/common/ListNode.java b/Week_01/common/ListNode.java new file mode 100644 index 00000000..eecc44e4 --- /dev/null +++ b/Week_01/common/ListNode.java @@ -0,0 +1,9 @@ +package Week_01.common; + +public class ListNode { + public int val; + public ListNode next; + public ListNode() {} + public ListNode(int val) { this.val = val; } + public ListNode(int val, ListNode next) { this.val = val; this.next = next; } +} diff --git a/Week_01/homework/DequeNewApiDemo.java b/Week_01/homework/DequeNewApiDemo.java new file mode 100644 index 00000000..a9c51822 --- /dev/null +++ b/Week_01/homework/DequeNewApiDemo.java @@ -0,0 +1,40 @@ +package Week_01.homework; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * @program: algorithm024 + * @description: 用 add first 或 add last 这套新的 API 改写 Deque 的代码 + * @author: xiaoboji + * @create: 2021-01-31 22:36 + **/ +public class DequeNewApiDemo { + public static void main(String[] args){ + // linkedList implements deque + // Old api + Queue deque = new LinkedList<>(); + deque.add(10000); + deque.add(20000); + deque.add(30000); + System.out.println("Deque:" + deque); + + // new api + LinkedList ll = new LinkedList(); + // Adding elements to the linked list + ll.add("A"); + ll.add("B"); + ll.addLast("C"); + ll.addFirst("D"); + ll.add(2, "E"); + + System.out.println(ll); + + ll.remove("B"); + ll.remove(3); + ll.removeFirst(); + ll.removeLast(); + + System.out.println(ll); + } +} diff --git a/Week_01/homework/MergeTwoArrays.java b/Week_01/homework/MergeTwoArrays.java new file mode 100644 index 00000000..04956aaa --- /dev/null +++ b/Week_01/homework/MergeTwoArrays.java @@ -0,0 +1,25 @@ +package Week_01.homework; + +/** + * @program: algorithm024 + * @description: 合并两个有序数组 + * @author: xiaoboji + * @create: 2021-02-01 00:32 + **/ +public class MergeTwoArrays { + + public void merge(int[] nums1, int m, int[] nums2, int n) { + while(n>0 && m>0){ + if(nums1[m-1] > nums2[n-1]){ + nums1[n+m-1] = nums1[m-1]; + m--; + }else{ + nums1[n+m-1] = nums2[n-1]; + n--; + } + } + for(int i = 0;i < n;i++){ + nums1[i] = nums2[i]; + } + } +} diff --git a/Week_01/homework/MergeTwoLists.java b/Week_01/homework/MergeTwoLists.java new file mode 100644 index 00000000..a6174133 --- /dev/null +++ b/Week_01/homework/MergeTwoLists.java @@ -0,0 +1,41 @@ +package Week_01.homework; + +import Week_01.common.ListNode; + +/** + * @program: algorithm024 + * @description: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 + * @author: xiaoboji + * @create: 2021-01-31 23:50 + **/ +public class MergeTwoLists { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + // 一些边界判断 + if(l1 == null){ + return l2; + } + if(l2 == null){ + return l1; + } + + // 链表技巧,使用头结点 + ListNode pre = new ListNode(-1); + ListNode head = pre; + + // 逐个比较,添加到pre链表去 + while(l1.next != null && l2.next != null){ + if(l1.val < l2.val){ + pre.next = l1; + l1 = l1.next; + } else { + pre.next = l2; + l2 = l2.next; + } + pre = pre.next; + + } + // 循环结束l1或者l2有尾巴 + pre.next = (l1==null? l2:l1); + return head.next; + } +} diff --git a/Week_01/homework/MoveZeros.java b/Week_01/homework/MoveZeros.java new file mode 100644 index 00000000..b108f6a4 --- /dev/null +++ b/Week_01/homework/MoveZeros.java @@ -0,0 +1,27 @@ +package Week_01.homework; + +/** + * @program: algorithm024 + * @description: 移动零 + * @author: xiaoboji + * @create: 2021-02-01 00:54 + **/ +public class MoveZeros { + // 快慢指针,类似于快排思想 + public void moveZeroes(int[] nums) { + if(nums.length == 0){ + return; + } + int j = 0; + for (int i = 0; i < nums.length; i++) { + if(nums[i] != 0){ + int temp = -1; + temp = nums[i]; + nums[i] = nums[j]; + nums[j++] = temp; + } + } + } + + +} diff --git a/Week_01/homework/PlusOne.java b/Week_01/homework/PlusOne.java new file mode 100644 index 00000000..a4bb838f --- /dev/null +++ b/Week_01/homework/PlusOne.java @@ -0,0 +1,26 @@ +package Week_01.homework; + +/** + * @program: algorithm024 + * @description: 加一 + * @author: xiaoboji + * @create: 2021-02-01 00:55 + **/ +public class PlusOne { + public int[] plusOne(int[] digits) { + // 两种情况9和非9,非9直接返回,9的话需要进位 + for (int i = 0; i < digits.length; i++){ + if(digits[digits.length - 1 - i] != 9){ + digits[digits.length - 1 - i]++; + return digits; + } else { + digits[digits.length - 1 - i] = 0; + } + } + + // 处理999的情况 + int[] result = new int[digits.length + 1]; + result[0] = 1; + return result; + } +} diff --git a/Week_01/homework/QueueDemo.java b/Week_01/homework/QueueDemo.java new file mode 100644 index 00000000..dd3a5d39 --- /dev/null +++ b/Week_01/homework/QueueDemo.java @@ -0,0 +1,14 @@ +package Week_01.homework; + +import java.util.PriorityQueue; +import java.util.Queue; + +/** + * @program: algorithm024 + * @description: study queue source code + * @author: xiaoboji + * @create: 2021-01-31 23:01 + **/ +public class QueueDemo { + Queue queue = new PriorityQueue(); +} diff --git a/Week_01/homework/README.md b/Week_01/homework/README.md new file mode 100644 index 00000000..c3ae98d8 --- /dev/null +++ b/Week_01/homework/README.md @@ -0,0 +1,81 @@ +### [用 add first 或 add last 这套新的 API 改写 Deque 的代码](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework) +```java +public class DequeNewApiDemo { + public static void main(String[] args){ + // linkedList implements deque + // Old api + Queue deque = new LinkedList<>(); + deque.add(10000); + deque.add(20000); + deque.add(30000); + System.out.println("Deque:" + deque); + + // new api + LinkedList ll = new LinkedList(); + // Adding elements to the linked list + ll.add("A"); + ll.add("B"); + ll.addLast("C"); + ll.addFirst("D"); + ll.add(2, "E"); + + System.out.println(ll); + + ll.remove("B"); + ll.remove(3); + ll.removeFirst(); + ll.removeLast(); + + System.out.println(ll); + } +} +``` +output: + +``` +Deque:[10000, 20000, 30000]
+[D, A, E, B, C]
+[A]
+``` + +### [分析 Queue 和 Priority Queue 的源码](https://github.com/xiaoboji/algorithm024/blob/main/Week_01/homework) +- 继承关系 + + |-Iterable(Interface)
+ |----Collection(Interface)
+ |-------Queue(Interface)
+ |----------Deque(Interface)
+ |-------------LinkedList(Class)
+ |-------------ArrayList(Class)
+ |----------PriorityQueue(Class)
+ +- Queue接口,相关方法如下所示 + * add(E e):将指定的元素插入到此队列中如果当前没有可用空间,则抛出IllegalStateException + * element:检索,但不删除,这个队列的头。 + * offer(E e):如果在不违反容量限制的情况下立即执行,则将指定的元素插入到此队列中。 + * peek:检索但不删除此队列的头,如果此队列为空,则返回 null + * poll:检索并删除此队列的头,如果此队列为空,则返回 null + * remove:检索并删除此队列的头 + +- PriorityQueue + * 简介 + + 优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序,也可以通过构造时传入的比较器. + + Java中PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。 + + * 关键点 + + 由于插入元素有优先级,所有有一个比较器的东西。其中的插入和弹出都做了自己的特有的处理,关键入口函数是siftUp和siftDown,这两个函数里面进行一些优先级的比较 + + * 方法列表 + + add:添加元素到队列中,队列满了会抛异常,其中调用offer函数进行插入 + + offer:插入元素到队列中,空就抛异常,原本没有元素就直接插入,有就调用siftUp函数进行插入 + + peek:直接返回头部元素 + + poll:获取并移除头部元素,siftDown函数返回数据 + + remove:其中调用了siftDown函数进行一些处理,移除头部元素 + * 参考链接 + + [深入理解Java PriorityQueue](https://www.cnblogs.com/CarpenterLee/p/5488070.html) + + [Java8 PriorityQueue 源码阅读](https://blog.csdn.net/codejas/article/details/85144502) + + [Queue Interface In Java](https://www.geeksforgeeks.org/queue-interface-java/) + diff --git a/Week_01/homework/TwoSum.java b/Week_01/homework/TwoSum.java new file mode 100644 index 00000000..0a0560eb --- /dev/null +++ b/Week_01/homework/TwoSum.java @@ -0,0 +1,20 @@ +package Week_01.homework; + +/** + * @program: algorithm024 + * @description: 两数之和 + * @author: xiaoboji + * @create: 2021-02-01 00:53 + **/ +public class TwoSum { + public int[] twoSum(int[] nums, int target) { + for(int i = 0; i < nums.length; i++){ + for (int j = i + 1; j < nums.length; j++){ + if(nums[i] + nums[j] == target){ + return new int[]{i, j}; + } + } + } + return null; + } +} diff --git a/Week_01/note/array.md b/Week_01/note/array.md new file mode 100644 index 00000000..cba2d488 --- /dev/null +++ b/Week_01/note/array.md @@ -0,0 +1,31 @@ +数组的知识总结 +--- + +> 经典的定义:数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。数组支持随机访问,根据下标随机访问的时间复杂度为 O(1) + +中间有三个核心概念 +- 线程表 + +线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。 + +与之相对应的是非线性表,非线性表中,数据之间并不是简单的前后关系,往往具有多前或者多后。比如二叉树、堆、图等。 +- 连续的内存空间 + +即使整体内存足够,但是数据不连续,也无法初始化需要大小的数组。 + +- 相同类型的数据 + +java中即使现在有了泛型,在泛型擦除之后,也还是具体的相同的数据类型。 + +小结: + +正是有了上述三个特点,才使得数组有了杀手锏一样的特性,随机访问。但是有利有弊,连续的内存空间和相同的数据类型,也使得数据的删除插入等操作低效,因为了保证连续性,就要移动大量的数据。 + +时间复杂度分析: + +操作 | 时间复杂度 +-------- | ----- +插入 | O(N) +删除 | O(N) +修改 | O(1) +查询 | O(1) diff --git a/Week_01/note/list.md b/Week_01/note/list.md new file mode 100644 index 00000000..c43fd2cc --- /dev/null +++ b/Week_01/note/list.md @@ -0,0 +1,28 @@ +链表的知识总结 +--- + +> 链表与数组恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是100MB大小的链表,根本不会有问题。 + +- 单链表 + +只记录后继节点信息的链表,习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点 + +- 双向链表 + +单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。 + +记录前后节点信息,查询比单链快,删除比单链简单,前插快,查找能类二分 + +- 循环链表 + +循环链表是一种特殊的单链表。实际上,循环链表也很简单。它跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。 + +和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。尽管用单链表也可以实现,但是用循环链表实现的话,代码就会简洁很多。 + +时间复杂度:正好和数据想法 +操作 | 时间复杂度 +-------- | ----- +插入 | O(1) +删除 | O(1) +修改 | O(N) +查询 | O(N) diff --git a/Week_01/note/queue.md b/Week_01/note/queue.md new file mode 100644 index 00000000..e3cbae6e --- /dev/null +++ b/Week_01/note/queue.md @@ -0,0 +1,7 @@ +队列的知识总结 +--- + +先进先出,后进后出,类似于排队买票。 + +最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。 + diff --git a/Week_01/note/skiplist.md b/Week_01/note/skiplist.md new file mode 100644 index 00000000..6b2f71fe --- /dev/null +++ b/Week_01/note/skiplist.md @@ -0,0 +1,4 @@ +跳表的知识总结 +--- + +Redis对于Set的优化实现方式,必须针对有序数据,指的是加多级索引的结构,支持类似“二分”的查找算法 diff --git a/Week_01/note/stack.md b/Week_01/note/stack.md new file mode 100644 index 00000000..5877aa4b --- /dev/null +++ b/Week_01/note/stack.md @@ -0,0 +1,13 @@ +栈的知识总结 +--- + +后进先出,先进后出,类似于叠盘子。 + +从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。 + +栈既可以通过数组实现,也可以通过链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。 + +不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1) + +栈只支持两个基本操作:入栈 push()和出栈 pop() + diff --git a/Week_01/pic/Queue-Deque-PriorityQueue-In-Java.png b/Week_01/pic/Queue-Deque-PriorityQueue-In-Java.png new file mode 100644 index 00000000..93dfda17 Binary files /dev/null and b/Week_01/pic/Queue-Deque-PriorityQueue-In-Java.png differ diff --git a/Week_02/README.md b/Week_02/README.md index 50de3041..aed17877 100644 --- a/Week_02/README.md +++ b/Week_02/README.md @@ -1 +1,115 @@ -学习笔记 \ No newline at end of file +本周学习了哈希、树、堆、图 + +#### 一、学习笔记 +- [哈希、映射、集合](https://github.com/xiaoboji/algorithm024/tree/main/Week_02/note/hash.md) +- [树、二叉树、二叉搜索树](https://github.com/xiaoboji/algorithm024/tree/main/Week_02/note/tree.md) +- [堆和堆排序](https://github.com/xiaoboji/algorithm024/tree/main/Week_02/note/heap.md) +- [图](https://github.com/xiaoboji/algorithm024/tree/main/Week_02/note/graph.md) + +#### 二、本周作业 + +简单: +- [写一个关于HashMap的小总结](https://github.com/xiaoboji/algorithm024/tree/main/Week_02/homework/HashMap.md) +- [有效的字母异位词(亚马逊、Facebook、谷歌在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_019_242_valid_anagram) +- [两数之和(非常多)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_003_1_two_sum) +- [N叉树的前序遍历(亚马逊在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_024_589_n_ary_tree_preorder_traversal) +- [HeapSort自学](https://github.com/xiaoboji/algorithm024/blob/main/Week_02/note/heap.md) + +中等: +- [字母异位词分组(亚马逊在半年内面试中常考)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_020_49_group_anagrams) +- [二叉树的中序遍历(亚马逊、字节跳动、微软在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_021_94_binary_tree_inorder_traversal) +- [二叉树的前序遍历(字节跳动、谷歌、腾讯在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_022_144_binary_tree_preorder_traversal) +- [N叉树的层序遍历(亚马逊在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_025_429_n_ary_tree_level_traversal) +- [丑数(字节跳动在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_026_49_chou_shu_lcof) +- [前K个高频元素(亚马逊在半年内面试中常考)] + +#### 三、训练场相关 +- 哈希相关: + * [找雪花] +- 树相关: + * [安装路灯] +- 二叉搜索树相关: + * [二叉搜索树的后序遍历序列] +- 堆相关: + * [最火视频榜单] +- 图相关: + * [手游上线] +#### 四、心得及小结 + +- 本周可以沉淀的代码 + * 二叉树遍历(递归) + ``` + // 前序遍历核心代码 + public void preorder(TreeNode root, List list) { + if (root == null) { + return; + } + list.add(root.val); + preorder(root.left, list); + preorder(root.right, list); + } + // 中序遍历核心代码 + public void inorder(TreeNode root, List list) { + if (root == null) { + return; + } + inorder(root.left, list); + list.add(root.val); + inorder(root.right, list); + } + // 后续遍历核心代码 + public void postorder(TreeNode root, List list) { + if (root == null) { + return; + } + postorder(root.left, list); + postorder(root.right, list); + list.add(root.val); + } + ``` + * 二叉树遍历(迭代) + ``` + // 前序遍历核心代码 + while(root != null || !stack.isEmpty()){ + //go left down to the ground + while(root != null){ + res.add(root.val); + stack.push(root); + root = root.left; + } + + //if we reach to the leaf, go back to the parent right, and repeat the go left down. + TreeNode cur = stack.pop(); + root = cur.right; + } + // 中序遍历核心代码 + while (root != null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left; + } + root = stack.pop(); + res.add(root.val); + root = root.right; + } + // 后续遍历核心代码,前序遍历的一个逆序 + while(root != null || !stack.isEmpty()){ + while(root != null){ + res.add(root.val); + stack.push(root); + root = root.right; + } + + TreeNode cur = stack.pop(); + root = cur.left; + } + Collections.reverse(res); + ``` + +- 本周学习心得 + +#### 五、疑问汇总 + +#### 六、练习汇总 + +[leetcode-做题记录](https://github.com/xiaoboji/j-leetcode) diff --git a/Week_02/homework/HashMap.md b/Week_02/homework/HashMap.md new file mode 100644 index 00000000..eb03dc2c --- /dev/null +++ b/Week_02/homework/HashMap.md @@ -0,0 +1,450 @@ +HashMap的学习笔记, 总结一下,顺便学学英语 + +--- +#### 一、HashMap简介 +> public class HashMap extends AbstractMap implements Map, Cloneable, Serializable + +继承了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口 + +> Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time. + +基于Hash表实现的MAP接口,实现了所有可选的的map操作,允许null值和null键.(HashMap类似于hashtable,不同之处在于hashmap是线程不安全的,并且允许空值),hashmap不保证顺序,具体点,不保证随时间推移,内部顺序保持不变。 + +> This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important. + +如果hash函数保证了元素均匀的分散,HashMap的常用操作(get和put)可以达到常数时间的性能,对集合进行迭代操作所需的时间,和HashMap实例的容量(桶的数量)以及大小(键值对数)成比例。所以,如果对迭代性能要求比较高的话,初始容量不要太高,装载因子不要太低。 + +> An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets. + +一个hashMap的实例,有两个重要的参数影响它的性能,初始容量和负载因子。这个容量就是hash算法中桶的数量,初始容量简而言之就是刚开始创建的时候的初始容量。这个负载因子就是当hash表的容满的时候,去创建hash表的一个度量值。当hash表里的元素个数超过了当前容量*负载因子,这个hash表就会重hash(初始的hash结构会被重构),hash表会扩充到原来的大致两倍的大小。 + +> As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur. + +常规来说,默认的负载因子(0.75)能兼顾时间和空间的开销。负载因子越大会减少空间的消耗,但是会增加查找的消耗(影响包括get和put在内的大部分HashMap的操作)。设置这个值要综合考虑表里的数据量和负载因子,以到达最少rehash的操作次数。如果初始容量太大,将永远不会rehash(这样浪费了空间)。 + +> If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table. Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table. To ameliorate impact, when keys are Comparable, this class may use comparison order among keys to help break ties. + +如果一个HashMap的实例需要存储很多映射关系,创建一个足够大的可以从初它的容量,比让它不停的rehash要更有效一些。很多key都有相同的hashcode会降低检索的性能。为了改善这个影响,当这些 +键值hashcode一样的时候,这个类需要比较这些减值以解决问题。 + +> Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the map. If no such object exists, the map should be "wrapped" using the Collections.synchronizedMap method. This is best done at creation time, to prevent accidental unsynchronized access to the map: + +需要注意的是,hashmap不是线程安全的,在多线程的条件下,有多个线程结构化修改这个map的值,那它必须在外部进行同步。(结构化修改是指,添加或者删除一个映射的相关操作,只是改变一一下值不是一个结构化修改)。通常使用同步这个对象来实现对hashmap不同步的支持。如果不存在这样的类,需要使用Collections.synchronizedMap方法,为了防止有不同步的情况出现,最好创建hashmap的时候就这么干。 + +> Map m = Collections.synchronizedMap(new HashMap(...)); + +> The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. + +HashMap的所有集合视图方法返回的迭代器都满足fail-fast机制,这个迭代器创建之后,map在任何时候被修改,只能通过迭代器励的remove方法,除此之外,会抛出一个ConcurrentModificationExceprion方。因此,面对这种并发修改的场景,迭代器快速的返回失败,而不是将来会莫名其妙的爆出一个不确定的行为或者错误。 + +> Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs. + +需要注意的是,fail-fast机制不能够保证没有问题,通常来说,也不可能完全保证对于一个不同步的去做非同步的并发修改,fail-fast迭代器尽最大努力的去抛出ConcurrentModificaitonException。因此,写程序去依赖这个异常是不对的,这个fail-fast机制应该仅仅用来解决bug。 + +#### 二、HashMap API详解 +返回类型 | 方法和描述 +-------- | ----- +void | clear()
移除map里的所有数据 +Object | clone()
返回一个hashmap的浅拷贝,具体的key值和values值不会被克隆 +V | compute(K key, BiFunction remappingFunction)
计算指定键值 +V | computeIfAbsent(K key, Function mappingFunction)
计算不存在的键值 +V | computeIfPresent(K key, BiFunction remappingFunction)
计算存在的键值 +boolean | containsKey(Object key)
如果map里包含这个key,则返回true +boolean | containsValue(Object value)
如果map里包含一个或多个key的value为这个值,则返回true +Set> | entrySet()
返回一个所有映射关系的Set +void | forEach(BiConsumer action)
一种简单的循环方式 +V | get(Object key)
获取值 +V | getOrDefault(Object key, V defaultValue)
获取值,如果不存在就返回默认值 +boolean | isEmpty()
是否为空,为空则返回ture +Set | keySet()
返回包含所有key的一个set +V | merge(K key, V value, BiFunction remappingFunction)
新老值合并指定key+value +V | put(K key, V value)
写入指定的key值 +void | putAll(Map m)
将另一个map里的值全部写入,如果有重复的key则覆盖原值 +V | putIfAbsent(K key, V value)
如果不存在则写入 +V | remove(Object key)
移除指定的key值 +boolean | remove(Object key, Object value)
移除指定的key+value +V | replace(K key, V value)
替换指定的key值 +boolean | replace(K key, V oldValue, V newValue)
替换指定的key+value值 +void | replaceAll(BiFunction function)
替换所有的value值 +int | size()
返回map的size +Collection | values()
返回所有的值的一个collection +#### 三、HashMap Test实例 +```java +/** + * HashMap API Test + * + * @author jixiaobo + */ +public class HashMapTest { + public static void main(String[] args){ + HashMap hashMap = new HashMap<>(16); + + // 存入值(>>>>:{1=A, 2=B, 3=C}) + hashMap.put("1","A"); + hashMap.put("2","B"); + hashMap.put("3","C"); + // putIfAbsent 当key不存在时添加, key存在时不做任何操作 + hashMap.putIfAbsent("1","A-putIfAbsent"); + System.out.println(hashMap); + + // 返回元素个数(>>>>:3) + int size = hashMap.size(); + System.out.println(size); + + // 获取值(>>>>:A) + String value = hashMap.get("1"); + System.out.println(value); + // 获取key值对应的value,当key不存在的时候,返回默认值(>>>>:default) + String def = hashMap.getOrDefault("5", "default"); + System.out.println(def); + + // 是否包含key/value值,是否为空(>>>>:true false false) + boolean bkey = hashMap.containsKey("1"); + boolean bvalue = hashMap.containsKey("A"); + boolean isempty = hashMap.isEmpty(); + System.out.println(bkey + " " + bvalue + " " + isempty); + + // putAll,讲一个具体的MAP中的映射关系全部复制到另一个map中,hashmap如果有相同的key,将会被覆盖(>>>>:{1=T1, 2=B, 3=C, 4=T}) + HashMap temp = new HashMap<>(2); + temp.put("4","T"); + temp.put("1","T1"); + hashMap.putAll(temp); + System.out.println(hashMap); + + // remove-移除key值或者key(>>>>:{1=T1, 3=C, 4=T}) + hashMap.remove("2"); + // remove-只有完全匹配才会被移除,下面的key=3 value=A不存在所有不被移除1 + hashMap.remove("3","A"); + System.out.println(hashMap); + + // keySet-获取hashmap中的所有key(>>>>:1 3 4 ) + Set keys = hashMap.keySet(); + for (String str : keys) { + System.out.print(str + " "); + } + System.out.println(); + + // keySet-获取hashmap中的所有值(>>>>: T1 C T ) + Collection values = hashMap.values(); + for (String str : values) { + System.out.print(str + " "); + } + System.out.println(); + + // replace-替换value的逻辑,(>>>>: {1=A-replace, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.replace("1", "A+"); + // replaceAll-替换所有的值为100 + hashMap.replaceAll((k,v) -> v = "A-replaceAll"); + // replace-匹配上key+value的值之后才会被替换 + hashMap.replace("1","A-replaceAll","A-replace"); + System.out.println(hashMap); + + // entrySet-获取所有值(>>>>: 1=A-replace 3=A-replaceAll 4=A-replaceAll ) + Set> mapEvtry = hashMap.entrySet(); + for (Entry entry : mapEvtry ) { + System.out.print(entry + " "); + } + System.out.println(); + + // forEach-lambda表达式,更简单的for循环(>>>>: 1 A-replace3 A-replaceAll4 A-replaceAll) + hashMap.forEach( + (k, v) -> { + System.out.print(k + " " + v); + }); + System.out.println(); + + // merge-合并value的旧值和新值(>>>>: {1=A-replacenew, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.merge("1", "new", (oldValue, newValue) -> oldValue + newValue); + System.out.println(hashMap); + + // 对不存在的key进行计算,如果是数值型 可以进行相应的数值计算(>>>>: {1=A-replacenew, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.computeIfAbsent("2", (key) -> "computeIfAbsent"); + System.out.println(hashMap); + // 对存在的key进行计算,如果是数值型 可以进行相应的数值计算(>>>>:{1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll} ) + hashMap.computeIfPresent("1", (key, oldvalue) -> oldvalue + " computeIfAbsent" ); + System.out.println(hashMap); + // 对指定key进行计算,不存在的时候添加,存在的时候修改(>>>>: {1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll, 6=null computeIfAbsent}) + hashMap.compute("6", (key, oldvalue) -> oldvalue + " computeIfAbsent" ); + System.out.println(hashMap); + + // clone,由当前的数据浅拷贝,key和value还是一份内存地址,不会被拷贝(>>>>: {1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll, 6=null computeIfAbsent}) + Object cloneMap = hashMap.clone(); + hashMap.put("1", "clone"); + hashMap.clear(); + System.out.println(cloneMap); + + } + +} +``` +#### 四、HashMap 关键源码解读 +- 链表节点 +``` + /** + * Basic hash bin node, used for most entries. (See below for + * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) + */ + static class Node implements Map.Entry { + final int hash;//hash值 + final K key;//key值 + V value;//value值 + Node next;//下一个节点 + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + // 每一个节点的hash值,是将key值的hash和value值的hash异或后的值 + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } + } +``` +hashmap的节点是使用node进行存储,使用的是一个单链表,单链表中每个节点的hash值是根据key和value的hashcode进行异或 + +- put()函数 +``` + /** + * Implements Map.put and related methods. + * + * @param hash hash for key + * @param key the key + * @param value the value to put + * @param onlyIfAbsent if true, don't change existing value//如果onlyifAbsent为true,不会覆盖相同key的值value + * @param evict if false, the table is in creation mode.// 如果evict是false, + * @return previous value, or null if none + */ + final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + // tab存储当前的hash桶,p作为临时链表节点 + Node[] tab; Node p; int n, i; + // table 被延迟到插入新数据时再进行初始化 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可 + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 发生了hash冲突 + else { + Node e; K k; + // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + // 如果hash值相同,节点又是通过红黑树存储的,则使用红黑树存储新节点 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 如果不需要覆盖,则插入一个普通的链表节点 + else { + // 对链表进行遍历,并统计链表长度 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { + // 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后 + p.next = newNode(hash, key, value, null); + // 如果节点的链表数量>=8,则转换为红黑树,以增加查询效率 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + // 条件为 true,表示当前链表包含要插入的键值对,终止遍历 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + // 判断要插入的键值对是否存在 HashMap 中 + if (e != null) { // existing mapping for key + // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值 + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + // 修改modCount + ++modCount; + // 键值对数量超过阈值时,则进行扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; + } +``` + +- 查询具体的key值 +``` + public V get(Object key) { + Node e; + // 根据key值获得hash值,根据hash值找具体的值 + return (e = getNode(hash(key), key)) == null ? null : e.value; + } + // 根据hash值和key找到目标节点ndoe + final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + // 如果hash表不为空,根据hash值计算出index(核心在此-->first = tab[(n - 1) & hash]) + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 如果index处只有一个值,即没有hash冲突,则直接返回这个值 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + // 如果index处有多个值 + if ((e = first.next) != null) { + // index的节点是一个树节点,则调用红黑树进行查找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + // 否则的话就使用链表逐个查找 + do { + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; + } +``` +- 扩容函数 + +``` +final Node[] resize() { + Node[] oldTab = table; + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + // 如果 table 不为空,表明已经初始化过了 + if (oldCap > 0) { + // 当 table 容量超过容量最大值,则不再扩容 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 按旧容量和阈值的2倍计算新容量和阈值的大小 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } else if (oldThr > 0) // initial capacity was placed in threshold + /* + * 初始化时,将 threshold 的值赋值给 newCap, + * HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值 + */ + newCap = oldThr; + else { // zero initial threshold signifies using defaults + /* + * 调用无参构造方法时,桶数组容量为默认容量, + * 阈值为默认容量与默认负载因子乘积 + */ + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + + // newThr 为 0 时,按阈值计算公式进行计算 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + // 创建新的桶数组,桶数组的初始化也是在这里完成的 + Node[] newTab = (Node[])new Node[newCap]; + table = newTab; + if (oldTab != null) { + // 如果旧的桶数组不为空,则遍历桶数组,并将键值对映射到新的桶数组中 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + // 重新映射时,需要对红黑树进行拆分 + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + // 遍历链表,并将链表节点按原顺序进行分组 + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 将分组后的链表映射到新桶中 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; +} + +``` +#### 五、HashMap 小结 +- hashmap的作用 + * HashMap 底层基于散列算法实现 + * HashMap 允许 null 键和 null 值 + * HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑树优化过长的链表。 + * HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化 + * HashMap 是非线程安全类,在多线程环境下可能会存在问题 +- putval() + * 当桶数组 table 为空时,通过扩容的方式初始化 table + * 查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值 + * 如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树 + * 判断键值对数量是否大于阈值,大于的话则进行扩容操作 +- get() + * 先定位键值对所在的桶的位置 + * 然后再对链表或红黑树进行查找 +- resize() + * HashMap桶数组的长度均是2的幂,阈值大小为桶数组长度与负载因子的乘积 + * 当HashMap中的键值对数量超过阈值时,进行扩容 + * 扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去 + +#### 六、参考链接 +- [HashMap 源码详细分析(JDK1.8)](https://segmentfault.com/a/1190000012926722) +- [HashMap API文档](https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html) \ No newline at end of file diff --git a/Week_02/homework/HashMapTest.java b/Week_02/homework/HashMapTest.java new file mode 100644 index 00000000..9de9ceb5 --- /dev/null +++ b/Week_02/homework/HashMapTest.java @@ -0,0 +1,115 @@ +package Week_02.homework; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * HashMap API Test + * + * @author jixiaobo + */ +public class HashMapTest { + public static void main(String[] args){ + HashMap hashMap = new HashMap<>(16); + + // 存入值(>>>>:{1=A, 2=B, 3=C}) + hashMap.put("1","A"); + hashMap.put("2","B"); + hashMap.put("3","C"); + // putIfAbsent 当key不存在时添加, key存在时不做任何操作 + hashMap.putIfAbsent("1","A-putIfAbsent"); + System.out.println(hashMap); + + // 返回元素个数(>>>>:3) + int size = hashMap.size(); + System.out.println(size); + + // 获取值(>>>>:A) + String value = hashMap.get("1"); + System.out.println(value); + // 获取key值对应的value,当key不存在的时候,返回默认值(>>>>:default) + String def = hashMap.getOrDefault("5", "default"); + System.out.println(def); + + // 是否包含key/value值,是否为空(>>>>:true false false) + boolean bkey = hashMap.containsKey("1"); + boolean bvalue = hashMap.containsKey("A"); + boolean isempty = hashMap.isEmpty(); + System.out.println(bkey + " " + bvalue + " " + isempty); + + // putAll,讲一个具体的MAP中的映射关系全部复制到另一个map中,hashmap如果有相同的key,将会被覆盖(>>>>:{1=T1, 2=B, 3=C, 4=T}) + HashMap temp = new HashMap<>(2); + temp.put("4","T"); + temp.put("1","T1"); + hashMap.putAll(temp); + System.out.println(hashMap); + + // remove-移除key值或者key(>>>>:{1=T1, 3=C, 4=T}) + hashMap.remove("2"); + // remove-只有完全匹配才会被移除,下面的key=3 value=A不存在所有不被移除1 + hashMap.remove("3","A"); + System.out.println(hashMap); + + // keySet-获取hashmap中的所有key(>>>>:1 3 4 ) + Set keys = hashMap.keySet(); + for (String str : keys) { + System.out.print(str + " "); + } + System.out.println(); + + // keySet-获取hashmap中的所有值(>>>>: T1 C T ) + Collection values = hashMap.values(); + for (String str : values) { + System.out.print(str + " "); + } + System.out.println(); + + // replace-替换value的逻辑,(>>>>: {1=A-replace, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.replace("1", "A+"); + // replaceAll-替换所有的值为100 + hashMap.replaceAll((k,v) -> v = "A-replaceAll"); + // replace-匹配上key+value的值之后才会被替换 + hashMap.replace("1","A-replaceAll","A-replace"); + System.out.println(hashMap); + + // entrySet-获取所有值(>>>>: 1=A-replace 3=A-replaceAll 4=A-replaceAll ) + Set> mapEvtry = hashMap.entrySet(); + for (Entry entry : mapEvtry ) { + System.out.print(entry + " "); + } + System.out.println(); + + // forEach-lambda表达式,更简单的for循环(>>>>: 1 A-replace3 A-replaceAll4 A-replaceAll) + hashMap.forEach( + (k, v) -> { + System.out.print(k + " " + v); + }); + System.out.println(); + + // merge-合并value的旧值和新值(>>>>: {1=A-replacenew, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.merge("1", "new", (oldValue, newValue) -> oldValue + newValue); + System.out.println(hashMap); + + // 对不存在的key进行计算,如果是数值型 可以进行相应的数值计算(>>>>: {1=A-replacenew, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll}) + hashMap.computeIfAbsent("2", (key) -> "computeIfAbsent"); + System.out.println(hashMap); + // 对存在的key进行计算,如果是数值型 可以进行相应的数值计算(>>>>:{1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll} ) + hashMap.computeIfPresent("1", (key, oldvalue) -> oldvalue + " computeIfAbsent" ); + System.out.println(hashMap); + // 对指定key进行计算,不存在的时候添加,存在的时候修改(>>>>: {1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll, 6=null computeIfAbsent}) + hashMap.compute("6", (key, oldvalue) -> oldvalue + " computeIfAbsent" ); + System.out.println(hashMap); + + // clone,由当前的数据浅拷贝,key和value还是一份内存地址,不会被拷贝(>>>>: {1=A-replacenew computeIfAbsent, 2=computeIfAbsent, 3=A-replaceAll, 4=A-replaceAll, 6=null computeIfAbsent}) + Object cloneMap = hashMap.clone(); + hashMap.put("1", "clone"); + hashMap.clear(); + System.out.println(cloneMap); + + } + +} diff --git a/Week_02/homework/README.md b/Week_02/homework/README.md new file mode 100644 index 00000000..dd3f2e2f --- /dev/null +++ b/Week_02/homework/README.md @@ -0,0 +1,10 @@ +本周学习了哈希、树、堆、图 + +# 一、学习笔记 + + +# 二、本周作业 + + +# 三、小结 + diff --git a/Week_02/note/graph.md b/Week_02/note/graph.md new file mode 100644 index 00000000..3a3e5382 --- /dev/null +++ b/Week_02/note/graph.md @@ -0,0 +1,102 @@ +图的知识总结 +--- +1. 图的属性和分类 +- 图的定义:有点有边 +- Graph(V,E) +- V - vertex:点 + * 度 - 入度和出度 + * 点与点之间:联通与否 +- E - edge:边 + * 有向和无向(单行线,双向度) + * 权重(边长) +2. 图的表示和分类(以无向无权图表示) +- 邻接数组 +- 邻接链表 +![avatar](../pic/graph01.jpg) + +3. 基于图相关的算法 +- DFS深度遍历代码 - 递归写法 + * 和树种的DFS最大的区别,不要忘记加visited = set() + * 代码模板 + ``` + public void bfs(int s, int t) { + if (s == t) return; + boolean[] visited = new boolean[v]; + visited[s]=true; + Queue queue = new LinkedList<>(); + queue.add(s); + int[] prev = new int[v]; + for (int i = 0; i < v; ++i) { + prev[i] = -1; + } + while (queue.size() != 0) { + int w = queue.poll(); + for (int i = 0; i < adj[w].size(); ++i) { + int q = adj[w].get(i); + if (!visited[q]) { + prev[q] = w; + if (q == t) { + print(prev, s, t); + return; + } + visited[q] = true; + queue.add(q); + } + } + } + } + + private void print(int[] prev, int s, int t) { // 递归打印s->t的路径 + if (prev[t] != -1 && t != s) { + print(prev, s, prev[t]); + } + System.out.print(t + " "); + } + + ``` +- BFS代码 - 递归写法 + * 代码模板 + ``` + public void bfs(int s, int t) { + if (s == t) return; + boolean[] visited = new boolean[v]; + visited[s]=true; + Queue queue = new LinkedList<>(); + queue.add(s); + int[] prev = new int[v]; + for (int i = 0; i < v; ++i) { + prev[i] = -1; + } + while (queue.size() != 0) { + int w = queue.poll(); + for (int i = 0; i < adj[w].size(); ++i) { + int q = adj[w].get(i); + if (!visited[q]) { + prev[q] = w; + if (q == t) { + print(prev, s, t); + return; + } + visited[q] = true; + queue.add(q); + } + } + } + } + + private void print(int[] prev, int s, int t) { // 递归打印s->t的路径 + if (prev[t] != -1 && t != s) { + print(prev, s, prev[t]); + } + System.out.print(t + " "); + } + ``` +- [连通图个数](https://leetcode-cn.com/problems/number-of-islands/) +- [拓扑排序(Topological Sorting)](https://zhuanlan.zhihu.com/p/34871092) +- [最短路径(Shortest Path):Dijkstra](https://www.bilibili.com/video/av25829980?from=search&seid=13391343514095937158) +- [最小生成树(Minimum Spanning Tree)](https://www.bilibili.com/video/av84820276?from=search&seid=17476598104352152051) + +4. 参考链接 +- [深度和广度优先搜索](https://time.geekbang.org/column/article/70891?utm_source=web&utm_medium=pinpaizhuanqu&utm_campaign=baidu&utm_term=pinpaizhuanqu&utm_content=0427) + + diff --git a/Week_02/note/hash.md b/Week_02/note/hash.md new file mode 100644 index 00000000..6ba77d40 --- /dev/null +++ b/Week_02/note/hash.md @@ -0,0 +1,57 @@ +哈希、映射、集合的知识总结 +--- + +- **hash table** + +哈希表,也叫散列表,是根据关键码值而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(hash function),存放记录的数据叫做哈希表(或散列表) + +- **工程实践** + * 电话号码簿 + * 用户信息表 + * 缓存(LRU Cache) + * 键值对存储(Redis) + * 安全加密(MD5,SHA,DES,AES算法等) + * 唯一标识(哈希算法可以对大数据做信息摘要,通过一个较短的二进制编码来表示很大的数据) + * 数据校验(校验数据的完整性和正确性) + * 负载均衡(一个会话粘滞(session sticky)的负载均衡算法,即在一次会话中的所有请求都路由到同一个服务器上) + * 分布式存储(利用一致性哈希算法,可以解决缓存等分布式系统的扩容、缩容导致数据大量搬移的难题) + * 数据分片(处理的海量数据进行分片,多机分布式处理,可以突破单机资源的限制) + +- **一个好的hash函数** + * 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法) + * 对输入数据非常敏感,哪怕原始数据只修改了一个bit,最后得到的哈希值也大不相同 + * 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率很小 + * 哈希算法的执行效率要尽量的高效,针对较长的文本,能快速的计算出哈希值 + +一个比较好的hash函数的例子就是MD5算法 +``` +MD5("今天我来讲哈希算法") = bb4767201ad42c74e650c1b6c03d78fa +MD5("jiajia") = cd611a31ea969b908932d44d126d195b +``` +- **一致性hash算法** + +假设我们有 k 个机器,数据的哈希值的范围是[0, MAX]。我们将整个范围划分成 m 个小区间(m 远大于 k),每个机器负责 m/k 个小区间。 + +当有新机器加入的时候,我们就将某几个小区间的数据,从原来的机器中搬移到新的机器中。这样,既不用全部重新哈希、搬移数据,也保持了各个机器上数据数量的均衡。 + +- **时间空间复杂度分析** + +平均复杂度:查询O(1);插入O(1);删除O(1); + +最坏复杂度:查询O(n);插入O(n);删除O(n); + +- **map和set** + * map(key-value对,key值不重复) + + new HashMap() + + map.set(key,value) + + map.get(key) + + map.has(key) + + map.size() + + map.clear() + + * set(不重复元素的集合) + + new HashSet() + + set.add(value) + + set.delete(value) + + set.has(value) + \ No newline at end of file diff --git a/Week_02/note/heap.md b/Week_02/note/heap.md new file mode 100644 index 00000000..1988ebcb --- /dev/null +++ b/Week_02/note/heap.md @@ -0,0 +1,170 @@ +堆和二叉堆的知识总结 +--- +1. 堆的概念 + +堆是一种特殊的树,可以迅速的找到一堆数中的最大或者最小值的数据结构。 + +假设是大顶堆,则常见的操作: +- find-max:O(1) +- delete-max:O(logN) +- insert(create): O(logN) or O(1) + +对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆” + +2. 二叉堆 + +- 二叉堆是一个完全二叉树 +- 二叉堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。 + +第一点,堆必须是一个完全二叉树。还记得我们之前讲的完全二叉树的定义吗?完全二叉树要求,除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。 + +第二点,堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。 + +3. 如何实现一个堆 + +完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。 +![avatar](../pic/heap01.png) + +[110,100,90,40,80,20,60,10,30,50,70] + +- 索引为i的左孩子的索引是(2*i + 1) +- 索引为i的右孩子的索引是(2*i + 2) +- 索引为i的父节点的索引是floor((i-1)/2) + +4. 二叉树的操作 + +- 插入 + * 放到堆尾 + * 自下往上堆化(heapify),顺着节点所在的路径,向上或者向下,对比然后交换 + +![avatar](../pic/heapifyup01.jpg) + +![avatar](../pic/heapifyup02.jpg) +``` +public class Heap { + private int[] a; // 数组,从下标1开始存储数据 + private int n; // 堆可以存储的最大数据个数 + private int count; // 堆中已经存储的数据个数 + + public Heap(int capacity) { + a = new int[capacity + 1]; + n = capacity; + count = 0; + } + + public void insert(int data) { + if (count >= n) return; // 堆满了 + ++count; + a[count] = data; + int i = count; + while (i/2 > 0 && a[i] > a[i/2]) { // 自下往上堆化 + swap(a, i, i/2); // swap()函数作用:交换下标为i和i/2的两个元素 + i = i/2; + } + } + } +``` +- 删除 + * 把最后一个节点放到堆顶 + * 然后从上往下的堆化方法(HeapifyDown) + +![avatar](../pic/heapifydown01.jpg) +``` +public void removeMax() { + if (count == 0) return -1; // 堆中没有数据 + a[1] = a[count]; + --count; + heapify(a, count, 1); +} + +private void heapify(int[] a, int n, int i) { // 自上往下堆化 + while (true) { + int maxPos = i; + if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2; + if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1; + if (maxPos == i) break; + swap(a, i, maxPos); + i = maxPos; + } +} +``` + +5. 堆排序 + +堆排序的过程大致分解成两个大的步骤,建堆和排序 +- 建堆 +数组原地建成一个堆。所谓“原地”就是,不借助另一个数组,就在原数组上操作。建堆的过程,有两种思路。 + +第一种是借助我们前面讲的,在堆中插入一个元素的思路。尽管数组中包含 n 个数据,但是我们可以假设,起初堆中只包含一个数据,就是下标为 1 的数据。然后,我们调用前面讲的插入操作,将下标从 2 到 n 的数据依次插入到堆中。这样我们就将包含 n 个数据的数组,组织成了堆。 + +第二种实现思路,跟第一种截然相反,非常妙。第一种建堆思路的处理过程是从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化。而第二种实现思路,是从后往前处理数组,并且每个数据都是从上往下堆化。 + +![avatar](../pic/heapsort01.jpg) + +![avatar](../pic/heapsort02.jpg) + +因为叶子节点往下堆化只能自己跟自己比较,所以我们直接从最后一个非叶子节点开始,依次堆化就行了。 + +``` + +private static void buildHeap(int[] a, int n) { + for (int i = n/2; i >= 1; --i) { + heapify(a, n, i); + } +} + +private static void heapify(int[] a, int n, int i) { + while (true) { + // 从非叶子节点开始 + int maxPos = i; + // 左右子结点与当前结点对比,找出最大 + if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2; + // 左右子结点与当前结点对比,找出最大 + if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1; + // heapify操作参数携带了数组长度和需要堆化的节点。 + if (maxPos == i) break; + swap(a, i, maxPos); + i = maxPos; + } +} +``` + +第二种方法,我们对下标从 2/n​ 开始到 1 的数据进行堆化,下标是 2/n​+1 到 n 的节点是叶子节点,我们不需要堆化 + +建堆的时间复杂度就是 O(n),推导过程详见参考链接。 + +- 排序 + +建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。 + +这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。 + +![avatar](../pic/heapsort03.jpg) + +``` +// n表示数据的个数,数组a中的数据从下标1到n的位置。 +public static void sort(int[] a, int n) { + buildHeap(a, n); + int k = n; + while (k > 1) { + swap(a, 1, k); + --k; + heapify(a, k, 1); + } +} +``` + +整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn) + +堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。 + +- 总结 + +堆排序包含两个过程,建堆和排序。我们将下标从 2n​ 到 1 的节点,依次进行从上到下的堆化操作,然后就可以将数组中的数据组织成堆这种数据结构。接下来,我们迭代地将堆顶的元素放到堆的末尾,并将堆的大小减一,然后再堆化,重复这个过程,直到堆中只剩下一个元素,整个数组中的数据就都有序排列了。 + +6. Java堆的实现 +- BinaryHeap +- PriorityQueue + +7. 参考链接 +- [堆和堆排序](https://time.geekbang.org/column/article/69913?utm_source=web&utm_medium=pinpaizhuanqu&utm_campaign=baidu&utm_term=pinpaizhuanqu&utm_content=0427) \ No newline at end of file diff --git a/Week_02/note/tree.md b/Week_02/note/tree.md new file mode 100644 index 00000000..994ccd23 --- /dev/null +++ b/Week_02/note/tree.md @@ -0,0 +1,157 @@ +树、二叉树、二叉搜索树的知识总结 +--- + +- **树** + +树和图都是二维的数据结构,二者的区别,是图有环,树没有环,树可以解决在单链表中查询过慢的问题。 + +Linked List是特殊化的Tree,Tree是特殊化的Graph + +树的定义 +```java +public class TreeNode { + public int val; + public TreeNode left; + public TreeNode right; + TreeNode() {} + TreeNode(int val) { this.val = val; } + TreeNode(int val, TreeNode left, TreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } +} +``` + +节点的高度(Height) = 节点到叶子节点的最长路径(边数)
+节点的深度深度(Depth)= 根节点到这个节点所经历的边的个数
+节点的层树(Level) = 节点的深度 + 1
+树的高度 = 根节点的高速
+ +- **树的遍历** + * 前序遍历:跟-左-右 + * 中序遍历:左-跟-右 + * 后序遍历:左-右-跟 + +- **二叉树** + + * 完全二叉树是二叉树的一种特殊情况,满二叉树又是完全二叉树的一种特殊情况 + * 二叉树: 顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。 + * 满二叉树: 编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点。 + * 完全二叉树: 叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。 + * 二叉树的存储: + + 基于指针的链式存储法:每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针,大部分二叉树代码都是通过这种结构来实现的。 + + 基于数组的顺序存储法:如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。 + +- **二叉搜索树** + +二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。 + +1. 查找: + +首先,我们看如何在二叉查找树中查找一个节点。我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。 +``` + public Node find(int data) { + Node p = tree; + while (p != null) { + if (data < p.data) p = p.left; + else if (data > p.data) p = p.right; + else return p; + } + return null; + } + +``` +2. 插入 + +二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。 + +如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。 + +``` +public void insert(int data) { + if (tree == null) { + tree = new Node(data); + return; + } + + Node p = tree; + while (p != null) { + if (data > p.data) { + if (p.right == null) { + p.right = new Node(data); + return; + } + p = p.right; + } else { // data < p.data + if (p.left == null) { + p.left = new Node(data); + return; + } + p = p.left; + } + } +} +``` +3. 删除 +二叉查找树的查找、插入操作都比较简单易懂,但是它的删除操作就比较复杂了 。 + +第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。 + +第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。 + +第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。 + +``` + +public void delete(int data) { + Node p = tree; // p指向要删除的节点,初始化指向根节点 + Node pp = null; // pp记录的是p的父节点 + while (p != null && p.data != data) { + pp = p; + if (data > p.data) p = p.right; + else p = p.left; + } + if (p == null) return; // 没有找到 + + // 要删除的节点有两个子节点 + if (p.left != null && p.right != null) { // 查找右子树中最小节点 + Node minP = p.right; + Node minPP = p; // minPP表示minP的父节点 + while (minP.left != null) { + minPP = minP; + minP = minP.left; + } + p.data = minP.data; // 将minP的数据替换到p中 + p = minP; // 下面就变成了删除minP了 + pp = minPP; + } + + // 删除节点是叶子节点或者仅有一个子节点 + Node child; // p的子节点 + if (p.left != null) child = p.left; + else if (p.right != null) child = p.right; + else child = null; + + if (pp == null) tree = child; // 删除的是根节点 + else if (pp.left == p) pp.left = child; + else pp.right = child; +} +``` + +- **时间复杂度分析** + +插入:O(logN)
+删除:O(logN)
+ +- **红黑树** + * 红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求: + * 根节点是黑色的 + * 每个叶子节点都是黑色的空节点,也就是说,叶子节点不存储数据 + * 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的 + * 每个节点,从改节点到达其可达节点的所有路径,都包含相同数目的黑色节点 + +- 参考链接 + * [二叉搜索树 Demo](https://visualgo.net/zh/bst?slide=1) + * [数据结构预算法之美](https://time.geekbang.org/column/article/68334?utm_source=web&utm_medium=pinpaizhuanqu&utm_campaign=baidu&utm_term=pinpaizhuanqu&utm_content=0427) + * [二叉搜索树demo](https://visualgo.net/zh/bst) diff --git a/Week_02/pic/graph01.jpg b/Week_02/pic/graph01.jpg new file mode 100644 index 00000000..91051911 Binary files /dev/null and b/Week_02/pic/graph01.jpg differ diff --git a/Week_02/pic/heap01.png b/Week_02/pic/heap01.png new file mode 100644 index 00000000..8ccaec9d Binary files /dev/null and b/Week_02/pic/heap01.png differ diff --git a/Week_02/pic/heapifydown01.jpg b/Week_02/pic/heapifydown01.jpg new file mode 100644 index 00000000..6cbc0775 Binary files /dev/null and b/Week_02/pic/heapifydown01.jpg differ diff --git a/Week_02/pic/heapifyup01.jpg b/Week_02/pic/heapifyup01.jpg new file mode 100644 index 00000000..eca9fe58 Binary files /dev/null and b/Week_02/pic/heapifyup01.jpg differ diff --git a/Week_02/pic/heapifyup02.jpg b/Week_02/pic/heapifyup02.jpg new file mode 100644 index 00000000..e41152f0 Binary files /dev/null and b/Week_02/pic/heapifyup02.jpg differ diff --git a/Week_02/pic/heapsort01.jpg b/Week_02/pic/heapsort01.jpg new file mode 100644 index 00000000..7500a872 Binary files /dev/null and b/Week_02/pic/heapsort01.jpg differ diff --git a/Week_02/pic/heapsort02.jpg b/Week_02/pic/heapsort02.jpg new file mode 100644 index 00000000..51e20633 Binary files /dev/null and b/Week_02/pic/heapsort02.jpg differ diff --git a/Week_02/pic/heapsort03.jpg b/Week_02/pic/heapsort03.jpg new file mode 100644 index 00000000..81d80a66 Binary files /dev/null and b/Week_02/pic/heapsort03.jpg differ diff --git a/Week_03/README.md b/Week_03/README.md index 50de3041..a1f4dad2 100644 --- a/Week_03/README.md +++ b/Week_03/README.md @@ -1 +1,29 @@ -学习笔记 \ No newline at end of file +#### 一、学习笔记 +- [递归](https://github.com/xiaoboji/algorithm024/tree/main/Week_03/note/Recursion.md) +- [分治](https://github.com/xiaoboji/algorithm024/tree/main/Week_03/note/DivideAndConquer.md) +- [回溯](https://github.com/xiaoboji/algorithm024/tree/main/Week_03/note/Backtracking.md) +#### 二、本周作业 + +- [二叉树的最近公共祖先(Facebook 在半年内面试常考)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_036_236_lowest_common_ancestor_of_a_binary_tree) +- [从前序与中序遍历序列构造二叉树(字节跳动、亚马逊、微软在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_037_105_onstruct_binary_tree_from_preorder_and_inorder_traversal) +- [组合(微软、亚马逊、谷歌在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_038_77_combinations) +- [全排列(字节跳动在半年内面试常考)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_039_46_permutations) +- [全排列 II (亚马逊、字节跳动、Facebook 在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problems/no_040_47_permutations_ii) + +#### 三、训练场练习 +- 递归相关: + * [指令计算器设计] + * [赛程表问题] +- 回溯相关: + * [单词转换] +#### 四、心得及小结 + +- 本周可以沉淀的代码 + +详见学习笔记 + +- 本周学习心得 + +递归、回溯有点难以理解,都是参考别人的实现 + +#### 五、疑问汇总 diff --git a/Week_03/note/Backtracking.md b/Week_03/note/Backtracking.md new file mode 100644 index 00000000..e6891cd0 --- /dev/null +++ b/Week_03/note/Backtracking.md @@ -0,0 +1,22 @@ +回溯知识总结 +--- + +- 概念 + +> 回溯法采用试错的思想,它尝试分步的去解决一个问题,在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至上几步的计算,再通过其它的可能的分步解答在此尝试寻找问题的答案。
+> +> 回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤之后会出现两种情况
+> 1. 找到一个可能存在的正确的答案
+> 2. 在尝试了所有可能的分步方法后宣告该问题没有答案
+> +> 在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
+ +简而言之,就是在每一层里去试就行了,最典型的是八皇后问题以及数独上面. + + +- 总结 + +回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。 + + + diff --git a/Week_03/note/DivideAndConquer.md b/Week_03/note/DivideAndConquer.md new file mode 100644 index 00000000..d819c542 --- /dev/null +++ b/Week_03/note/DivideAndConquer.md @@ -0,0 +1,28 @@ +分治知识总结 +--- +- 分治是一种特殊的递归,归根到底就是找重复性 + * 最优重复性:动态规划 + * 最近重复性:分治、回溯等 + +- 和递归非要说有什么差别的话,可以理解为拆分完之后,有个合并的过程 + +- 代码模板 +``` +private static int divide_conquer(Problem, problem){ + // 到达最小问题,叶子结点 + if (problem == null) { + int res = process_last_result(); + return res; + } + // 处当前逻辑,如何把这个大问题拆分成小问题 + subProlems = split_prolem(problem); + // 下探一层,解决更细节的问题 + res0 = divide_conquer(subProblems[0]); + res1 = divide_conquer(subProblems[1]); + // 对结果数据进行合并 + result = process_result(res0, res1); + // 返回结果 + return result; +} +``` + diff --git a/Week_03/note/Recursion.md b/Week_03/note/Recursion.md new file mode 100644 index 00000000..41235176 --- /dev/null +++ b/Week_03/note/Recursion.md @@ -0,0 +1,36 @@ +递归知识总结 +--- +- 递归需要满足的三个条件 + + * 一个问题的解可以分解为几个子问题的解 + * 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样 + * 存在递归终止条件 + +递归是一种非常高效、简洁的编码技巧。只要是满足“三个条件”的问题就可以通过递归代码来解决。 + +递归代码虽然简洁高效,但是,递归代码也有很多弊端。比如,堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等,所以,在编写递归代码的时候,一定要控制好这些副作用。 + +- 几个关键点 + * 编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。 + * 函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。 + * 不要人肉进行递归 + * 找最近重复子问题 + * 数学归纳法的思维 +- Java代码模板 +``` +public void recur(int level, int param) { + //terminator + if (level > MAX_LEVEL) { + // process result + return; + } + // process current logic + process(level, param); + + // drill down + recur(level: level + 1, newParam); + + // restore current status +} +``` + \ No newline at end of file diff --git a/Week_04/README.md b/Week_04/README.md index 50de3041..a197c130 100644 --- a/Week_04/README.md +++ b/Week_04/README.md @@ -1 +1,46 @@ -学习笔记 \ No newline at end of file +#### 一、学习笔记 +- [深度+广度优先遍历](https://github.com/xiaoboji/algorithm024/tree/main/Week_04/note/BfsDfs.md) +- [贪心算法](https://github.com/xiaoboji/algorithm024/tree/main/Week_04/note/Greedy.md) +- [二分查找](https://github.com/xiaoboji/algorithm024/tree/main/Week_04/note/BinarySearch.md) +#### 二、本周作业 + +- 简单: + * [柠檬水找零(亚马逊在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/greedy/d_860/[860]柠檬水找零.java) + * 买卖股票的最佳时机 II (亚马逊、字节跳动、微软在半年内面试中考过) + * [分发饼干(亚马逊在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/tree/main/java/src/main/java/com/xiaoboji/problem/greedy/d_455/[455]分发饼干.java) + * 模拟行走机器人 + * 使用二分查找,寻找一个半有序数组 [4, 5, 6, 7, 0, 1, 2] 中间无序的地方 +- 中等: + * 单词接龙(亚马逊在半年内面试常考) + * 岛屿数量(近半年内,亚马逊在面试中考查此题达到 350 次) + * 扫雷游戏(亚马逊、Facebook 在半年内面试中考过) + * 跳跃游戏 (亚马逊、华为、Facebook 在半年内面试中考过) + * 搜索旋转排序数组(Facebook、字节跳动、亚马逊在半年内面试常考) + * 搜索二维矩阵(亚马逊、微软、Facebook 在半年内面试中考过) + * 寻找旋转排序数组中的最小值(亚马逊、微软、字节跳动在半年内面试中考过) +- 困难 + * 单词接龙 II (微软、亚马逊、Facebook 在半年内面试中考过) + * 跳跃游戏 II (亚马逊、华为、字节跳动在半年内面试中考过) + +#### 三、训练场练习f +- 二分法相关: + * 分数统计 +- DFS、BFS 相关: + * 二叉树纵向逆序遍历 + * 包裹容量问题 + * IP还原问题 + * 任务处理问题 + * 追赶朋友 +- 贪心相关: + * 饥饿的老鼠 +#### 四、心得及小结 + +- 本周可以沉淀的代码 + + +- 本周学习心得 + + +#### 五、疑问汇总 +- 一些题目的时间复杂度分析不来 +- \ No newline at end of file diff --git a/Week_04/note/BfsDfs.md b/Week_04/note/BfsDfs.md new file mode 100644 index 00000000..ecfb86da --- /dev/null +++ b/Week_04/note/BfsDfs.md @@ -0,0 +1,38 @@ +深度/广度优先算法知识总结 +--- + +- 深度优先算法 + +深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。 + +``` +void dfs(TreeNode root) { + if (root == null) { + return; + } + dfs(root.left); + dfs(root.right); +} + +``` + +- 广度优先算法 + +广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现 + +``` +void bfs(TreeNode root) { + Queue queue = new ArrayDeque<>(); + queue.add(root); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); // Java 的 pop 写作 poll() + if (node.left != null) { + queue.add(node.left); + } + if (node.right != null) { + queue.add(node.right); + } + } +} + +``` \ No newline at end of file diff --git a/Week_04/note/BinarySearch.md b/Week_04/note/BinarySearch.md new file mode 100644 index 00000000..6bb9f8a2 --- /dev/null +++ b/Week_04/note/BinarySearch.md @@ -0,0 +1,66 @@ +二分查找知识总结 +--- +- 二分查找的前提 + * 目标单数单调性(单调递增或者递减) + * 存在上下界(bounded) + * 能够通过索引/下标访问(index accessible),简单点说就是数组。 + +- 代码模板 + +三个容易出错的地方:循环退出条件、mid 的取值,low 和 high 的更新。 + +``` + // 循环实现 + public int binarySearch(int[] array, int target) + { + int left = 0, right = array.length - 1, mid; + while (left <= right) { + mid = (right - left) / 2 + left; + if (array[mid] == target) { + return mid; + } else if (array[mid] > target) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return -1; + } +``` + +``` + +// 二分查找的递归实现 + public int bsearch(int[] a, int n, int val) { + return bsearchInternally(a, 0, n - 1, val); + } + + private int bsearchInternally(int[] a, int low, int high, int value) { + if (low > high) return -1; + + int mid = low + ((high - low) >> 1); + if (a[mid] == value) { + return mid; + } else if (a[mid] < value) { + return bsearchInternally(a, mid+1, high, value); + } else { + return bsearchInternally(a, low, mid-1, value); + } + } +``` +- 经验总结 + * 二分查找依赖的是顺序表结构,简单点说就是数组 + * 二分查找针对的是有序数据 + * 数据量太小不适合二分查找,顺序遍历就够了 + * 数据量太大也不适合二分查找,要用数组,就要用大量的连续空间 + +- 典型应用 + * 1000 万个整数数据(内存限制是 100MB,每个数据大小是 8 字节),快速判断某个整数是否出现在这 1000 万数据中。 + * 快速定位IP对应的省份地址 + * 查找第一个值等于给定值的元素 + * 查找最后一个值等于给定值的元素 + * 查找第一个大于等于给定值的元素 + * 查找最后一个小于等于给定值的元素 + +- 参考链接 + * [Fast InvSqrt() 扩展阅读](https://www.beyond3d.com/content/articles/8/) \ No newline at end of file diff --git a/Week_04/note/Greedy.md b/Week_04/note/Greedy.md new file mode 100644 index 00000000..397a7b04 --- /dev/null +++ b/Week_04/note/Greedy.md @@ -0,0 +1,28 @@ +贪心算法知识总结 +--- + + - 概念 + +贪心算法(英语:greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。 + +贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。 + +- 经验技巧 + +贪心算法理论消化不了,直接练习最好。 + +贪心算法的最难的一块是如何将要解决的问题抽象成贪心算法模型,只要这一步搞定之后,贪心算法的编码一般都很简单。 + +实现该算法的过程:
+从问题的某一初始解出发;while 能朝给定总目标前进一步 do,求出可行解的一个解元素
+最后,由所有解元素组合成问题的一个可行解。
+ +- 经典应用 + * 霍夫曼编码(Huffman Coding) + * Prim算法 + * Kruskal 最小生成树算法 + * Dijkstra 单源最短路径算法 + +- 参考资料 + * [贪心算法wiki](https://zh.wikipedia.org/wiki/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95) + diff --git a/Week_06/README.md b/Week_06/README.md index 50de3041..af23eb0a 100644 --- a/Week_06/README.md +++ b/Week_06/README.md @@ -1 +1,29 @@ -学习笔记 \ No newline at end of file +#### 一、学习笔记 +- [动态规划](https://github.com/xiaoboji/algorithm024/tree/main/Week_06/note/DynamicProgramming.md) +#### 二、本周作业 +- 中等 + * [最小路径和(亚马逊、高盛集团、谷歌在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/dynamic_programming/[64]最小路径和.java) + * 解码方法(亚马逊、Facebook、字节跳动在半年内面试中考过) + * [最大正方形(华为、谷歌、字节跳动在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/dynamic_programming/[221]最大正方形.java) + * 任务调度器(Facebook 在半年内面试中常考) + * 回文子串(Facebook、苹果、字节跳动在半年内面试中考过) +- 困难 + * 最长有效括号(字节跳动、亚马逊、微软在半年内面试中考过) + * 编辑距离(字节跳动、亚马逊、谷歌在半年内面试中考过) + * 矩形区域不超过 K 的最大数值和(谷歌在半年内面试中考过) + * 青蛙过河(亚马逊、苹果、字节跳动在半年内面试中考过) + * 分割数组的最大值(谷歌、亚马逊、Facebook 在半年内面试中考过) + * 学生出勤记录 II (谷歌在半年内面试中考过) + * 最小覆盖子串(Facebook 在半年内面试中常考) + * 戳气球(亚马逊在半年内面试中考过) + +#### 三、心得及小结 + +- 本周可以沉淀的代码 + + +- 本周学习心得 + + +#### 四、疑问汇总 +- 一些题目的时间复杂度分析不来 \ No newline at end of file diff --git a/Week_06/note/DynamicProgramming.md b/Week_06/note/DynamicProgramming.md new file mode 100644 index 00000000..e5e1e5dc --- /dev/null +++ b/Week_06/note/DynamicProgramming.md @@ -0,0 +1,2 @@ +动态规划学习总结 +--- diff --git a/Week_08/README.md b/Week_08/README.md index 50de3041..76c39e89 100644 --- a/Week_08/README.md +++ b/Week_08/README.md @@ -1 +1,28 @@ -学习笔记 \ No newline at end of file +#### 一、学习笔记 +- [红黑树与AVL树](https://github.com/xiaoboji/algorithm024/tree/main/Week_06/note/DynamicProgramming.md) +- [位运算](https://github.com/xiaoboji/algorithm024/tree/main/Week_06/note/DynamicProgramming.md) +#### 二、本周作业 +- 简单 + * [位 1 的个数(Facebook、苹果在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/bit_manipulation/191.md) + * [2 的幂(谷歌、亚马逊、苹果在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/bit_manipulation/231.md) + * 颠倒二进制位(苹果在半年内面试中考过) +- 中等 + * 实现 Trie (前缀树) (亚马逊、微软、谷歌在半年内面试中考过) + * 省份数量(亚马逊、Facebook、字节跳动在半年内面试中考过) + * 岛屿数量(近半年内,亚马逊在面试中考查此题达到 361 次) + * 被围绕的区域(亚马逊、eBay、谷歌在半年内面试中考过) +- 困难 + * 单词搜索 II (亚马逊、微软、苹果在半年内面试中考过) + * N 皇后(字节跳动、亚马逊、百度在半年内面试中考过) + * N 皇后 II (亚马逊在半年内面试中考过) + +#### 三、心得及小结 + +- 本周可以沉淀的代码 + + +- 本周学习心得 + + +#### 四、疑问汇总 +- 一些题目的时间复杂度分析不来 \ No newline at end of file diff --git "a/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README.md" "b/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README.md" new file mode 100644 index 00000000..ec0cd40e --- /dev/null +++ "b/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README.md" @@ -0,0 +1,28 @@ +#### 一、学习笔记 +- [红黑树与AVL树](https://github.com/xiaoboji/algorithm024/tree/main/Week_06/note/DynamicProgramming.md) +- [位运算](https://github.com/xiaoboji/algorithm024/tree/main/Week_06/note/DynamicProgramming.md) +#### 二、本周作业 +- 简单 + * [位 1 的个数(Facebook、苹果在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/bit_manipulation/191.md) + * [2 的幂(谷歌、亚马逊、苹果在半年内面试中考过)](https://github.com/xiaoboji/j-leetcode/blob/main/java/src/main/java/com/xiaoboji/problem/bit_manipulation/231.md) + * 颠倒二进制位(苹果在半年内面试中考过) +- 中等 + * 实现 Trie (前缀树) (亚马逊、微软、谷歌在半年内面试中考过) + * 省份数量(亚马逊、Facebook、字节跳动在半年内面试中考过) + * 岛屿数量(近半年内,亚马逊在面试中考查此题达到 361 次) + * 被围绕的区域(亚马逊、eBay、谷歌在半年内面试中考过) +- 困难 + * 单词搜索 II (亚马逊、微软、苹果在半年内面试中考过) + * N 皇后(字节跳动、亚马逊、百度在半年内面试中考过) + * N 皇后 II (亚马逊在半年内面试中考过) + +#### 三、心得及小结 + +- 本周可以沉淀的代码 + + +- 本周学习心得 + + +#### 四、疑问汇总 +- 一些题目的时间复杂度分析不来 \ No newline at end of file diff --git "a/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README_SUMMARY.md" "b/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README_SUMMARY.md" new file mode 100644 index 00000000..8692243a --- /dev/null +++ "b/Week_10 \346\257\225\344\270\232\346\200\273\347\273\223/README_SUMMARY.md" @@ -0,0 +1,51 @@ +极客大学「算法训练营-第24期」学习总结 +--- +## 学习内容 +- 知识内容 + * 数组 + * 链表 + * 跳表 + * 栈 + * 队列 + * 哈希、映射、集合 + * 树、二叉树、二叉搜索树 + * 堆和堆排序 + * 图 + * 递归 + * 分治 + * 回溯 + * 深度+广度优先遍历 + * 贪心算法 + * 二分查找 + * 动态规划 + * Trie树 + * 红黑树 + * AVL树 + * 位运算 + * 布隆过滤器 + * LRU缓存 + * 排序算法 + * 字符串算法 +- 五毒神掌,五遍刷题法,强调练习算法题要过遍数,算法题不仅是做一遍通过而已,而是要通过刻意练习达到对算法熟练的效果。 + * 五分钟没思路,直接看优秀题解 + * 第一遍可以参考优秀答案 + * 第二遍自己默写,比较国内优秀解法 + * 第三遍看国际站,比较国际优秀解法 + * 第四遍默写+总结,记录优秀的解法 + * 第五遍用时前快速熟悉 +- 四步解题法 + * clarification 沟通题目/审题/边界条件/输入输出 + * possible solutions --> 所有的解法都思考一遍,optimal找到一种最优的解法(时间空间复杂度分析,自顶向下+边界+图解) + * code 确认后,编码 + * test cases 测试用例 + +## 学习收获 + +- 对整个的知识体系结构有了比较清晰的认识 +- 中期之前比较认真,中期之后投入就比较少了,有点不好意思 +- 核心的核心,不懂就看,坚持重复 +- 同步学习一些java的源码 +- 配合争哥的课程,对理论理解比较好 + + +