diff --git a/README.md b/README.md index d9720a7..0a49274 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,15 @@ 『技术博客』:www.frankfeekr.cn | 『开源贡献』:⊱ 英雄招募令 | 『微信订阅号』:全栈开发社区

+

-
+🔥🔥🔥 + +欢迎光临 LinTools 开发者的在线导航: https://tools.frankfeekr.cn + +如果你有更好的在线工具,[请点击留言](https://github.com/frank-lam/fullstack-tutorial/issues/65),持续更新! @@ -105,7 +110,7 @@ - [前端知识体系](notes/Frontend/前端知识体系.md) - [Angular 基础知识](notes/Frontend/Angular.md) -- [ES6+ 语法全解析](http://es.xiecheng.live/) +- [ES6+ 语法全解析](https://notes.frankfeekr.cn/docs/frontend/es6/%E9%A1%B9%E7%9B%AE%E5%87%86%E5%A4%87/%E5%89%8D%E8%A8%80) diff --git a/assets/zhishixingqiu.JPG b/assets/zhishixingqiu.JPG new file mode 100644 index 0000000..afdc791 Binary files /dev/null and b/assets/zhishixingqiu.JPG differ diff --git a/notes/data-structures-and-algorithms/README.md b/notes/data-structures-and-algorithms/README.md index 63d36e7..e824a53 100644 --- a/notes/data-structures-and-algorithms/README.md +++ b/notes/data-structures-and-algorithms/README.md @@ -1,8 +1,10 @@ -
- # 数据结构与算法 -## 学习路径 +> 欢迎来到数据结构与算法的世界,带你从基础数据结构层层深入。本章以 Java 代码为例,结合 LeetCode 题解示例进行数据结构学习。 + + + +## 学习目录 | 部分 | 章节 | 概要 | | :--- | :--------------------------- | :--------------------------------------------------- | @@ -12,20 +14,38 @@ -## 参考资料 +## 在线刷题 - 题库 - 力扣 (LeetCode) 全球极客挚爱的技术成长平台 + https://leetcode-cn.com/problemset/all +- 牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决 + + https://www.nowcoder.com/ + + + +## 在线工具 + +- VisuAlgo - visualising data structures and algorithms through animation + + https://visualgo.net/en + +- Data Structure Visualization + + https://www.cs.usfca.edu/~galles/visualization/Algorithms.html + ## 学习课程 -| 课程 | 推荐 | -| ------------------------------------------------------------ | ----------------------------------------- | -| [【慕课网】刘宇波:玩转数据结构,从入门到进阶](https://coding.imooc.com/class/207.html) | 数据结构从底层到实现,浅显易懂 | -| [【慕课网】刘宇波:程序员的内功修炼,学好算法与数据结构](https://coding.imooc.com/class/71.html) | 程序员的内功修炼,强烈推荐 | -| [【慕课网】刘宇波:玩转算法面试 leetcode题库分门别类详细解析](https://coding.imooc.com/class/82.html) | LeetCode 刷题入门,强烈推荐 | -| [【极客时间】覃超:算法面试通关40讲](https://time.geekbang.org/course/intro/130) | 市面上比较新的课程,推荐 | -| [【慕课网】刘宇波:看得见的算法 7个经典应用诠释算法精髓](https://coding.imooc.com/class/138.html) | 通过7款经典好玩游戏,真正将算法用于实际开 | +| 课程 | 链接 | 代码 | 推荐 | +| ------------------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------- | +| 【慕课网】玩转数据结构 从入门到进阶(Java) | [官网](https://coding.imooc.com/class/chapter/207.html) | [代码](https://github.com/liuyubobobo/Play‑with‑Data‑Structures) | 数据结构从底层到实现,浅显易懂 | +| 【慕课网】算法与数据结构体系课(Java)
| [官网](https://class.imooc.com/datastructure#Anchor) | [代码](https://github.com/frank‑lam/Algorithm) | 《玩转数据结构 从入门到进阶》 - 升级版 | +| 【慕课网】程序员的内功修炼,学好算法与数据结构(C++) | [官网](https://coding.imooc.com/class/chapter/71.html) | [代码](https://github.com/liuyubobobo/Play‑with‑Algorithms) | 程序员的内功修炼,强烈推荐 | +| 【慕课网】玩转算法面试 LeetCode 题库(C++) | [官网](https://coding.imooc.com/class/chapter/82.html) | [代码](https://github.com/liuyubobobo/Play‑with‑Algorithm‑Interview) | LeetCode 刷题入门,强烈推荐 | +| 【极客时间】覃超:算法面试通关40讲 | [官网](https://time.geekbang.org/course/intro/130) | / | 市面上比较新的课程,推荐 | +| 【慕课网】刘宇波:看得见的算法 7个经典应用诠释算法精髓 | [官网](https://coding.imooc.com/class/chapter/138.html) | / | 通过7款经典好玩游戏,真正将算法用于实际开 | diff --git a/notes/data-structures-and-algorithms/assets/image-20210821164803357.png b/notes/data-structures-and-algorithms/assets/image-20210821164803357.png new file mode 100644 index 0000000..1b18aee Binary files /dev/null and b/notes/data-structures-and-algorithms/assets/image-20210821164803357.png differ diff --git a/notes/data-structures-and-algorithms/assets/image-20210821164814332.png b/notes/data-structures-and-algorithms/assets/image-20210821164814332.png new file mode 100644 index 0000000..1b18aee Binary files /dev/null and b/notes/data-structures-and-algorithms/assets/image-20210821164814332.png differ diff --git a/notes/data-structures-and-algorithms/assets/image-20210821164819266.png b/notes/data-structures-and-algorithms/assets/image-20210821164819266.png new file mode 100644 index 0000000..1b18aee Binary files /dev/null and b/notes/data-structures-and-algorithms/assets/image-20210821164819266.png differ diff --git a/notes/data-structures-and-algorithms/assets/image-20210821172111765.png b/notes/data-structures-and-algorithms/assets/image-20210821172111765.png new file mode 100644 index 0000000..5ab3b50 Binary files /dev/null and b/notes/data-structures-and-algorithms/assets/image-20210821172111765.png differ diff --git "a/notes/data-structures-and-algorithms/leetcode/Leetcode 307. \345\214\272\345\237\237\345\222\214\346\243\200\347\264\242 - \346\225\260\347\273\204\345\217\257\344\277\256\346\224\271.md" "b/notes/data-structures-and-algorithms/leetcode/Leetcode 307. \345\214\272\345\237\237\345\222\214\346\243\200\347\264\242 - \346\225\260\347\273\204\345\217\257\344\277\256\346\224\271.md" new file mode 100644 index 0000000..78b304a --- /dev/null +++ "b/notes/data-structures-and-algorithms/leetcode/Leetcode 307. \345\214\272\345\237\237\345\222\214\346\243\200\347\264\242 - \346\225\260\347\273\204\345\217\257\344\277\256\346\224\271.md" @@ -0,0 +1,213 @@ +https://leetcode-cn.com/problems/range-sum-query-mutable + +## 307. 区域和检索 - 数组可修改 + +给你一个数组 `nums` ,请你完成两类查询,其中一类查询要求更新数组下标对应的值,另一类查询要求返回数组中某个范围内元素的总和。 + +实现 `NumArray` 类: + +- `NumArray(int[] nums)` 用整数数组 `nums` 初始化对象 +- `void update(int index, int val)` 将 `nums[index]` 的值更新为 `val` +- `int sumRange(int left, int right)` 返回子数组 `nums[left, right]` 的总和(即,`nums[left] + nums[left + 1], ..., nums[right]`) + + + +**示例:** + +``` +输入: +["NumArray", "sumRange", "update", "sumRange"] +[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]] +输出: +[null, 9, null, 8] + +解释: +NumArray numArray = new NumArray([1, 3, 5]); +numArray.sumRange(0, 2); // 返回 9 ,sum([1,3,5]) = 9 +numArray.update(1, 2); // nums = [1,2,5] +numArray.sumRange(0, 2); // 返回 8 ,sum([1,2,5]) = 8 +``` + + + +**提示:** + +- `1 <= nums.length <= 3 * 104` +- `-100 <= nums[i] <= 100` +- `0 <= index < nums.length` +- `-100 <= val <= 100` +- `0 <= left <= right < nums.length` +- 最多调用 `3 * 104` 次 `update` 和 `sumRange` 方法 + + + +**提交代码:** + +```java +class NumArray { + public class SegmentTree { + + private E[] tree; + private E[] data; + private Merger merger; + + public SegmentTree(E[] arr, Merger merger){ + + this.merger = merger; + + data = (E[])new Object[arr.length]; + for(int i = 0 ; i < arr.length ; i ++) + data[i] = arr[i]; + + tree = (E[])new Object[4 * arr.length]; + buildSegmentTree(0, 0, arr.length - 1); + } + + // 在treeIndex的位置创建表示区间[l...r]的线段树 + private void buildSegmentTree(int treeIndex, int l, int r){ + + if(l == r){ + tree[treeIndex] = data[l]; + return; + } + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + + // int mid = (l + r) / 2; + int mid = l + (r - l) / 2; + buildSegmentTree(leftTreeIndex, l, mid); + buildSegmentTree(rightTreeIndex, mid + 1, r); + + tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); + } + + public int getSize(){ + return data.length; + } + + public E get(int index){ + if(index < 0 || index >= data.length) + throw new IllegalArgumentException("Index is illegal."); + return data[index]; + } + + // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引 + private int leftChild(int index){ + return 2*index + 1; + } + + // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引 + private int rightChild(int index){ + return 2*index + 2; + } + + // 返回区间[queryL, queryR]的值 + public E query(int queryL, int queryR){ + + if(queryL < 0 || queryL >= data.length || + queryR < 0 || queryR >= data.length || queryL > queryR) + throw new IllegalArgumentException("Index is illegal."); + + return query(0, 0, data.length - 1, queryL, queryR); + } + + // 在以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值 + private E query(int treeIndex, int l, int r, int queryL, int queryR){ + + if(l == queryL && r == queryR) + return tree[treeIndex]; + + int mid = l + (r - l) / 2; + // treeIndex的节点分为[l...mid]和[mid+1...r]两部分 + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + if(queryL >= mid + 1) + return query(rightTreeIndex, mid + 1, r, queryL, queryR); + else if(queryR <= mid) + return query(leftTreeIndex, l, mid, queryL, queryR); + + E leftResult = query(leftTreeIndex, l, mid, queryL, mid); + E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR); + return merger.merge(leftResult, rightResult); + } + + // 将index位置的值,更新为e + public void set(int index, E e){ + + if(index < 0 || index >= data.length) + throw new IllegalArgumentException("Index is illegal"); + + data[index] = e; + set(0, 0, data.length - 1, index, e); + } + + // 在以treeIndex为根的线段树中更新index的值为e + private void set(int treeIndex, int l, int r, int index, E e){ + + if(l == r){ + tree[treeIndex] = e; + return; + } + + int mid = l + (r - l) / 2; + // treeIndex的节点分为[l...mid]和[mid+1...r]两部分 + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + if(index >= mid + 1) + set(rightTreeIndex, mid + 1, r, index, e); + else // index <= mid + set(leftTreeIndex, l, mid, index, e); + + tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); + } + + @Override + public String toString(){ + StringBuilder res = new StringBuilder(); + res.append('['); + for(int i = 0 ; i < tree.length ; i ++){ + if(tree[i] != null) + res.append(tree[i]); + else + res.append("null"); + + if(i != tree.length - 1) + res.append(", "); + } + res.append(']'); + return res.toString(); + } + } + + public interface Merger { + E merge(E a, E b); + } + + private SegmentTree segTree; + + public NumArray(int[] nums) { + + if(nums.length != 0){ + Integer[] data = new Integer[nums.length]; + for(int i = 0 ; i < nums.length ; i ++) + data[i] = nums[i]; + segTree = new SegmentTree<>(data, (a, b) -> a + b); + } + } + + public void update(int i, int val) { + if(segTree == null) + throw new IllegalArgumentException("Error"); + segTree.set(i, val); + } + + public int sumRange(int i, int j) { + if(segTree == null) + throw new IllegalArgumentException("Error"); + return segTree.query(i, j); + } +} +``` \ No newline at end of file diff --git "a/notes/data-structures-and-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/notes/data-structures-and-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" index e69de29..b3ffc0c 100644 --- "a/notes/data-structures-and-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ "b/notes/data-structures-and-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,286 @@ +# 数据结构 + +## 一、基础数据结构 + +### 字符串 + +- 标准库,解析,匹配等 + +### 线性表 + +- 数组 + +- 动态数组 + +### 栈和队列 + +### 链表 + +### 哈希表 + + + +## 二、高级数据结构 + +### 1. 线段树 + +线段树(segment tree),顾名思义, 是用来存放给定区间(segment, or interval)内对应信息的一种数据结构。与树状数组(binary indexed tree)相似,线段树也用来处理数组相应的区间查询(range query)和元素更新(update)操作。与树状数组不同的是,线段树不止可以适用于区间求和的查询,也可以进行区间最大值,区间最小值(Range Minimum/Maximum Query problem)或者区间异或值的查询。 + +对应于树状数组,线段树进行更新(update)的操作为 O(logn),进行区间查询(range query)的操作也为 O(logn)。 + +![image-20210821164803357](assets/image-20210821164803357.png) + +在线可视化:https://visualgo.net/en/segmenttree + + + +#### 303. 区域和检索 - 数组不可变 (LeetCode) + + +https://leetcode-cn.com/problems/range-sum-query-immutable + +```java +class NumArray { + private int[] data; + + public NumArray(int[] nums) { + this.data = new int[nums.length]; + for (int i = 0; i < nums.length; i++) { + this.data[i] = nums[i]; + } + } + + public void update(int index, int val) { + this.data[index] = val; + } + + public int sumRange(int left, int right) { + int sum = 0; + for (int i = left; i <= right; i++) { + sum += this.data[i]; + } + return sum; + } +} +``` + + + +#### 307. 区域和检索 - 数组可修改 (LeetCode) + +https://leetcode-cn.com/problems/range-sum-query-mutable + +```java + +class NumArray { + public class SegmentTree { + private E[] tree; + private E[] data; + private Merger merger; + + public SegmentTree(E[] arr, Merger merger){ + + this.merger = merger; + + data = (E[])new Object[arr.length]; + for(int i = 0 ; i < arr.length ; i ++) + data[i] = arr[i]; + + tree = (E[])new Object[4 * arr.length]; + buildSegmentTree(0, 0, arr.length - 1); + } + + // 在treeIndex的位置创建表示区间[l...r]的线段树 + private void buildSegmentTree(int treeIndex, int l, int r){ + + if(l == r){ + tree[treeIndex] = data[l]; + return; + } + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + + // int mid = (l + r) / 2; + int mid = l + (r - l) / 2; + buildSegmentTree(leftTreeIndex, l, mid); + buildSegmentTree(rightTreeIndex, mid + 1, r); + + tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); + } + + public int getSize(){ + return data.length; + } + + public E get(int index){ + if(index < 0 || index >= data.length) + throw new IllegalArgumentException("Index is illegal."); + return data[index]; + } + + // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引 + private int leftChild(int index){ + return 2*index + 1; + } + + // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引 + private int rightChild(int index){ + return 2*index + 2; + } + + // 返回区间[queryL, queryR]的值 + public E query(int queryL, int queryR){ + + if(queryL < 0 || queryL >= data.length || + queryR < 0 || queryR >= data.length || queryL > queryR) + throw new IllegalArgumentException("Index is illegal."); + + return query(0, 0, data.length - 1, queryL, queryR); + } + + // 在以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值 + private E query(int treeIndex, int l, int r, int queryL, int queryR){ + + if(l == queryL && r == queryR) + return tree[treeIndex]; + + int mid = l + (r - l) / 2; + // treeIndex的节点分为[l...mid]和[mid+1...r]两部分 + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + if(queryL >= mid + 1) + return query(rightTreeIndex, mid + 1, r, queryL, queryR); + else if(queryR <= mid) + return query(leftTreeIndex, l, mid, queryL, queryR); + + E leftResult = query(leftTreeIndex, l, mid, queryL, mid); + E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR); + return merger.merge(leftResult, rightResult); + } + + // 将index位置的值,更新为e + public void set(int index, E e){ + + if(index < 0 || index >= data.length) + throw new IllegalArgumentException("Index is illegal"); + + data[index] = e; + set(0, 0, data.length - 1, index, e); + } + + // 在以treeIndex为根的线段树中更新index的值为e + private void set(int treeIndex, int l, int r, int index, E e){ + + if(l == r){ + tree[treeIndex] = e; + return; + } + + int mid = l + (r - l) / 2; + // treeIndex的节点分为[l...mid]和[mid+1...r]两部分 + + int leftTreeIndex = leftChild(treeIndex); + int rightTreeIndex = rightChild(treeIndex); + if(index >= mid + 1) + set(rightTreeIndex, mid + 1, r, index, e); + else // index <= mid + set(leftTreeIndex, l, mid, index, e); + + tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); + } + + @Override + public String toString(){ + StringBuilder res = new StringBuilder(); + res.append('['); + for(int i = 0 ; i < tree.length ; i ++){ + if(tree[i] != null) + res.append(tree[i]); + else + res.append("null"); + + if(i != tree.length - 1) + res.append(", "); + } + res.append(']'); + return res.toString(); + } + } + + public interface Merger { + E merge(E a, E b); + } + + private SegmentTree segTree; + + public NumArray(int[] nums) { + + if(nums.length != 0){ + Integer[] data = new Integer[nums.length]; + for(int i = 0 ; i < nums.length ; i ++) + data[i] = nums[i]; + segTree = new SegmentTree<>(data, (a, b) -> a + b); + } + } + + public void update(int i, int val) { + if(segTree == null) + throw new IllegalArgumentException("Error"); + segTree.set(i, val); + } + + public int sumRange(int i, int j) { + if(segTree == null) + throw new IllegalArgumentException("Error"); + return segTree.query(i, j); + } +} +``` + + + +### 2. Trie + +https://blog.csdn.net/jt102605/article/details/84258314 + +https://blog.csdn.net/longgeqiaojie304/article/details/106316103 + +发生在微软的一个真实案例: + +在一个古老的手持设备中实现一个通讯录功能,但是当时的手持设备的芯片运算能力是非常低的,所以他们发现当通讯录中记录的条目非常多的时候,搜索通讯录中的内容是非常慢的。但是这个问题是被微软的一个实习生解决了。其实他解决的方式非常简单,他就是使用了这种Trie数据结构来解决的。 + + +![image-20210821172111765](assets/image-20210821172111765.png) + +### 树和二叉树 + +- 二叉树 + +- 二分搜索树 + +- AVL 树 + +- B 和 B+ + +- 二分查找法 + - Leetcode 704. + +- 红黑树 + + + +### 集合和映射 + + + +### 优先队列和堆 + + + + + +### Trie + +### 并查集 diff --git "a/notes/data-structures-and-algorithms/\347\256\227\346\263\225\346\200\235\346\203\263.md" "b/notes/data-structures-and-algorithms/\347\256\227\346\263\225\346\200\235\346\203\263.md" index 75410cf..28a3493 100644 --- "a/notes/data-structures-and-algorithms/\347\256\227\346\263\225\346\200\235\346\203\263.md" +++ "b/notes/data-structures-and-algorithms/\347\256\227\346\263\225\346\200\235\346\203\263.md" @@ -1,2 +1,730 @@ # 算法思想 +## 一、排序算法 + +### 1. 选择排序(Selection Sort) + +选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 + +![](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015224719590-1433219824.gif) + +**代码实现** + +```java +public static void sort(int[] arr) { + for (int i = 0; i < arr.length; i++) { + // 寻找[i, n)区间里的最小值的索引 + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if(arr[minIndex] > arr[j]){ + minIndex = j; + } + } + swap( arr , i , minIndex); + } +} + +private static void swap(int[] arr, int i, int j) { + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +**算法分析** + +表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。 + + + +### 2. 插入排序(Insertion Sort) + +插入排序从左到右进行,每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左部数组依然有序。 + +第 j 元素是通过不断向左比较并交换来实现插入过程:当第 j 元素小于第 j - 1 元素,就将它们的位置交换,然后令 j 指针向左移动一个位置,不断进行以上操作。 + +![](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015225645277-1151100000.gif) + +**代码实现** + +```java +public static void sort(int[] arr) { + for (int i = 0; i < arr.length; i++) { + // 选择 arr[i...n) 中的最小值 + int minIndex = i; + for (int j = i + 1; j < arr.length; j++) { + if (arr[minIndex] > arr[j]) { + minIndex = j; + } + } + swap(arr, i, minIndex); + } +} + +// 改进版插入排序(减少了数组元素的操作次数) +public static void better_sort(int[] arr) { + for (int i = 0; i < arr.length; i++) { + int e = arr[i]; + int j = i; + for (; j > 0; j--) { + if (e < arr[j - 1]) + arr[j] = arr[j - 1]; + else + break; + } + arr[j] = e; + } +} + +private static void swap(int[] arr, int i, int j) { + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + +**算法分析** + +插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 + + + +### 3. 冒泡排序(Bubble Sort) + +通过从左到右不断交换相邻逆序的相邻元素,在一轮的交换之后,可以让未排序的元素上浮到右侧。 + +在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 + +![](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015223238449-2146169197.gif) + +**代码实现** + +```java +private static void sort(int[] arr) { + for (int i = arr.length - 1; i > 0; i--) { // 从最后一位开始确定 + boolean swapped = false; + for (int j = 0; j < i; j++) { + if(arr[j] > arr[j+1]){ + swapped = true; + swap(arr,j,j+1); + } + } + if(!swapped) + return; + } +} + +private static void swap(int[] arr, int i, int j) { + int t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; +} +``` + + + +### 4. 希尔排序(Shell Sort) + +1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫**缩小增量排序**。 + + + +**算法描述** + +先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述: + +- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; +- 按增量序列个数k,对序列进行k 趟排序; +- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。 + +![](https://images2018.cnblogs.com/blog/849589/201803/849589-20180331170017421-364506073.gif) + +**代码实现** + +```java +// 希尔排序 +public static void sort(int[] arr) { + int n = arr.length; + for (int h = n / 2; h > 0; h = h / 2) { + // 内部是一个插入排序 + for (int i = 0; i < n; i = i + h) { + + int e = arr[i]; + int j = i; + for (; j > 0; j = j - h) { + if (e < arr[j - h]) + arr[j] = arr[j - h]; + else + break; + } + arr[j] = e; + } + } +} + + +// 希尔排序2 +public static void sort2(int[] arr) { + int n = arr.length; + // 计算 increment sequence: 1, 4, 13, 40, 121, 364, 1093... + int h = 1; + while (h < n / 3) h = 3 * h + 1; + + System.out.println(h); + + while (h >= 1) { + // h-sort the array + for (int i = h; i < n; i++) { + + // 对 arr[i], arr[i-h], arr[i-2*h], arr[i-3*h]... 使用插入排序 + int e = arr[i]; + int j = i; + for (; j >= h && e < arr[j - h]; j -= h) + arr[j] = arr[j - h]; + arr[j] = e; + } + + h /= 3; + } +} +``` + +**算法分析** + +对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。 + +希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 + +希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 + + + +### 5. 归并排序(Merge Sort) + +归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。把长度为n的输入序列分成两个长度为n/2的子序列;对这两个子序列分别采用归并排序;将两个排序好的子序列合并成一个最终的排序序列。 + +![](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015230557043-37375010.gif) + +**代码实现** + +> 1.归并方法 +> +> 归并方法将数组中两个已经排序的部分归并成一个。 + +```java +private static void sort(int[] arr) { + __MergeSort(arr, 0, arr.length - 1); +} + +private static void __MergeSort(int[] arr, int l, int r) { + if (l >= r) + return; + int mid = (l + r) / 2; + __MergeSort(arr, l, mid); + __MergeSort(arr, mid + 1, r); + merge(arr, l, mid, r); +} + +// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 +private static void merge(int[] arr, int l, int mid, int r) { + int[] aux = Arrays.copyOfRange(arr, l, r + 1); + + // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 + int i = l, j = mid + 1; + for (int k = l; k <= r; k++) { + if (i > mid) { // 如果左半部分元素已经全部处理完毕 + arr[k] = aux[j - l]; + j++; + } else if (j > r) { // 如果右半部分元素已经全部处理完毕 + arr[k] = aux[i - l]; + i++; + } else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素 + arr[k] = aux[i - l]; + i++; + } else { // 左半部分所指元素 >= 右半部分所指元素 + arr[k] = aux[j - l]; + j++; + } + } +} +``` + +> 2.自底向上归并排序 + +```java +private static void sort(int[] arr) { + int N = arr.length; + int[] aux = new int[N]; + for (int sz = 1; sz < N; sz += sz) + for (int i = 0; i + sz < N; i += sz + sz) + merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, N - 1)); +} +``` + + + +### 6. 快速排序(Quick Sort) + +快速排序可以说是20世纪最伟大的算法之一了。相信都有所耳闻,它的速度也正如它的名字那样,是一个非常快的算法了。当然它也后期经过了不断的改进和优化,才被公认为是一个值得信任的非常优秀的算法。 + +![](https://images2017.cnblogs.com/blog/849589/201710/849589-20171015230936371-1413523412.gif) + +**代码实现** + +#### 1. 普通快速排序 + +```java +// 递归使用快速排序,对arr[l...r]的范围进行排序 +public static void QuickSort(int[] arr,int l,int r){ + if(l>=r) + return; + int p = partition(arr,l,r); + QuickSort(arr,l,p-1); + QuickSort(arr,p+1,r); +} + +// 将数组通过p分割成两部分 +// 对arr[l...r]部分进行partition操作 +// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] +public static int partition(int[] arr, int l, int r) { + swap(arr, l, (int) (Math.random() % (r - l + 1)) + l); // 加入这一行变成随机快速排序 + + int v = arr[l]; + int j = l; + for(int i = j +1;i<=r;i++){ + if(arr[i] < v){ + j++; + swap(arr,i,j); + } + } + swap(arr,l,j); + return j; +} + +public static void swap(int[] arr,int i,int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; +} +``` + +快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。 + +快速排序最好的情况下是每次都正好能将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 CN=2CN/2+N,复杂度为 O(NlogN)。 + +最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N2/2。为了防止数组最开始就是有序的,在进行快速排序时需要随机打乱数组。 + + + +#### 2. 双路快速排序 + +若果数组中含有大量重复的元素,则partition很可能把数组划分成两个及其不平衡的两部分,时间复杂度退化成O(n²)。这时候应该把小于v和大于v放在数组两端。 + +![](../pics/partition2.jpg) + +```java +// 双路快速排序的partition +// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] +private static int partition(int[] arr, int l, int r) { + + // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot + // swap(arr, l, (int) (Math.random() % (r - l + 1)) + l); + + int v = arr[l]; + + // arr[l+1...i) <= v; arr(j...r] >= v + int i = l + 1, j = r; + while (true) { + // 注意这里的边界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0 + // 思考一下为什么? + while (i <= r && arr[i] < v) + i++; + + // 注意这里的边界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0 + // 思考一下为什么? + while (j >= l + 1 && arr[j] > v) + j--; + + // 对于上面的两个边界的设定, 有的同学在课程的问答区有很好的回答:) + // 大家可以参考: http://coding.imooc.com/learn/questiondetail/4920.html + if (i > j) + break; + + swap(arr, i, j); + i++; + j--; + } + + swap(arr, l, j); + + return j; +} + +// 递归使用快速排序,对arr[l...r]的范围进行排序 +private static void QuickSort2Ways(int[] arr, int l, int r) { + // 对于小规模数组, 使用插入排序 + if (l >= r) return; + int p = partition(arr, l, r); + QuickSort2Ways(arr, l, p - 1); + QuickSort2Ways(arr, p + 1, r); +} +``` + +#### 3. 三路快速排序 + +数组分成三个部分,大于v 等于v 小于v + +在具有大量重复键值对的情况下使用三路快排 + +![](../pics/partition3.jpg) + + + +```java +// 递归使用快速排序,对arr[l...r]的范围进行排序 +private static void QuickSort3Ways(int[] arr, int l, int r){ + + // 随机在arr[l...r]的范围中, 选择一个数值作为标定点pivot + swap( arr, l, (int)(Math.random()*(r-l+1)) + l ); + + int v = arr[l]; + + int lt = l; // arr[l+1...lt] < v + int gt = r + 1; // arr[gt...r] > v + int i = l+1; // arr[lt+1...i) == v + while( i < gt ){ + if( arr[i] < v){ + swap( arr, i, lt+1); + i ++; + lt ++; + } + else if( arr[i] > v ){ + swap( arr, i, gt-1); + gt --; + } + else{ // arr[i] == v + i ++; + } + } + swap( arr, l, lt ); + + QuickSort3Ways(arr, l, lt-1); + QuickSort3Ways(arr, gt, r); +} +``` + + + +### 7. 堆排序(Heap Sort) + +#### 1. 堆 + +堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。 + +堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。 + +![](../pics/heap.png) + +#### 2. 上浮和下沉 + +在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为**上浮(ShiftUp)**。 + +![](../pics/shiftup_heap.png) + +```java +private void shiftUp(int k){ + while( k > 1 && data[k/2] < data[k])){ + swap(k, k/2); + k /= 2; + } +} +``` + + + +类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为**下沉(Shift Down)**。一个节点如果有两个子节点,应当与两个子节点中最大那么节点进行交换。 + +![](../pics/shiftdown_heap.png) + +```java +private void shiftDown(int k){ + while( 2*k <= count ){ // 当前结点有左孩子 + int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置 + if( j+1 <= count && data[j+1] > data[j] ) + j ++; + // data[j] 是 data[2*k]和data[2*k+1]中的最大值 + + if( data[k] >= data[j] ) + break; + swap(k, j); + k = j; + } +} +``` + +#### 3.插入元素 + +将新元素放到数组末尾,然后上浮到合适的位置。 + +```java +// 向最大堆中插入一个新的元素 item +public void insert(Item item){ + assert count + 1 <= capacity; + data[count+1] = item; + count ++; + shiftUp(count); +} +``` + +#### 4. 删除最大元素 + +```java +// 从最大堆中取出堆顶元素, 即堆中所存储的最大数据 +public Item extractMax(){ + assert count > 0; + Item ret = data[1]; + + swap( 1 , count ); + count --; + shiftDown(1); + return ret; +} +``` + +#### 5. 堆排序 + +由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。 + +```java +// 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序 +public class HeapSort { + + // 对整个arr数组使用HeapSort1排序 + // HeapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序 + // 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn) + // 整个堆排序的整体时间复杂度为O(nlogn) + public static void sort1(Comparable[] arr){ + + int n = arr.length; + MaxHeap maxHeap = new MaxHeap(n); + for( int i = 0 ; i < n ; i ++ ) + maxHeap.insert(arr[i]); + + for( int i = n-1 ; i >= 0 ; i -- ) + arr[i] = maxHeap.extractMax(); + } + + + // 只通过shiftDown操作进行排序 + public static void sort2(Comparable[] arr){ + int n = arr.length; + + // 注意,此时我们的堆是从0开始索引的 + // 从(最后一个元素的索引-1)/2开始 + // 最后一个元素的索引 = n-1 + for( int i = (n-1-1)/2 ; i >= 0 ; i -- ) + shiftDown2(arr, n, i); + + for( int i = n-1; i > 0 ; i-- ){ // 这个的目的是让序列从小到大排序 + swap( arr, 0, i); + shiftDown2(arr, i, 0); + } + } + + // 交换堆中索引为i和j的两个元素 + private static void swap(Object[] arr, int i, int j){ + Object t = arr[i]; + arr[i] = arr[j]; + arr[j] = t; + } + + // 原始的shiftDown过程 + private static void shiftDown(Comparable[] arr, int n, int k){ + while( 2*k+1 < n ){ + int j = 2*k+1; + if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 ) + j += 1; + + if( arr[k].compareTo(arr[j]) >= 0 )break; + + swap( arr, k, j); + k = j; + } + } + + // 优化的shiftDown过程, 使用赋值的方式取代不断的swap, + // 该优化思想和我们之前对插入排序进行优化的思路是一致的 + private static void shiftDown2(Comparable[] arr, int n, int k){ + + Comparable e = arr[k]; + while( 2*k+1 < n ){ + int j = 2*k+1; + if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 ) + j += 1; + + if( e.compareTo(arr[j]) >= 0 ) + break; + + arr[k] = arr[j]; + k = j; + } + + arr[k] = e; + } + + // 测试 HeapSort + public static void main(String[] args) { + Integer[] arr = {10, 91, 8, 7, 6, 5, 4, 3, 2, 1}; + HeapSort.sort2(arr); + PrintHelper.printArray(arr); + } +} +``` + +#### 6. 堆排序的应用——Top K问题 + +例如,有1亿个浮点数,如何找出其中最大的10000个?(B326) + + + +### 8. 计数排序 + +https://www.cnblogs.com/freedom314/p/5847092.html + + + +### 9. 排序算法总结 + +| | 平均时间复杂度 | 原地排序 | 额外空间 | 稳定排序 | +| :------: | :------------: | :------: | :------: | :------: | +| 插入排序 | O(n^2) | √ | O(1) | √ | +| 归并排序 | O(nlogn) | × | O(n) | √ | +| 快速排序 | O(nlogn) | √ | O(logn) | × | +| 堆排序 | O(nlogn) | √ | O(1) | × | + +稳定排序:对于相等的元素,在排序后,原来靠前的元素依然靠前。相等元素的相对位置没有发生变化。 + +```java +// 可以通过⾃自定义⽐比较函数,让排序算法不不存在稳定性的问题。 +bool operator<(const Student& otherStudent){ + return score != otherStudent.score ? + score > otherStudent.score : + name < otherStudent.name; +} +``` + + + + + + + +![](../pics/sort_algorithm_analyze.png) + + + +## 二、二分查找法 + +Leetcode 704. + + + + + +## 三、递归和回溯法 + +- Leetcode 70. + +- Leetcode 17. + + + +## 四、分治的思想 + + + + + +## 五、动态规划 + +- 9-1 什么是动态规划 (20:27) + +- 9-2 第一个动态规划问题 Climbing Stairs (14:02) + + - [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/) + + - 【作业】[120. 三角形最小路径和](https://leetcode-cn.com/problems/triangle/) + + - 【作业】[64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/) + +- 9-3 发现重叠子问题 Integer Break (25:10) + + - [343. 整数拆分](https://leetcode-cn.com/problems/integer-break/) + + - 【作业】279 + + - 【作业】91 + + - 【作业】62 + + - 【作业】63 + +- 9-4 状态的定义和状态转移 House Robber (27:19) + + - [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/) + + - 【作业】213 + + - 【作业】337 + + - 【作业】309 + +- 9-5 0-1背包问题 (32:37) + +- 9-6 0-1背包问题的优化和变种 (20:18) + +- 9-7 面试中的0-1背包问题 Partition Equal Subset Sum (27:34) + +- 9-8 LIS问题 Longest Increasing Subsequence (25:12) + + - [300. 最长递增子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) + + - 【作业】[376. 摆动序列](https://leetcode-cn.com/problems/wiggle-subsequence/) + +- 9-9 LCS,最短路,求动态规划的具体解以及更多 (21:00) + - [1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/) + +- 9-10 动态规划的经典问题 + +- https://www.cxyzjd.com/article/qq_42874319/115740445 + +- + +## 六、贪心算法 08/10 + +- 10-1 贪心基础 Assign Cookies (12:12) + + - [455. 分发饼干](https://leetcode-cn.com/problems/assign-cookies/) + + - 【作业】[392. 判断子序列](https://leetcode-cn.com/problems/is-subsequence/) + +- 10-2 贪心算法与动态规划的关系 Non-overlapping Intervals (17:58) + - [435. 无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/) + +- 10-3 贪心选择性质的证明 (15:19) + + + +## 七、模式匹配 + +- KMP + +- RK + + + +## 八、搜索算法(深搜,广搜等) + + + +## 九、并查集