算法与数据结构基础总结

Github
内容来自刘宇波老师算法与数据结构体系课

1、线性查找
2、选择排序、插入排序
3、动态数组
4、循环队列
5、链表
6、递归链表
7、归并排序
8、快速排序
9、二分查找
10、二分搜索树
11、堆
12、冒泡排序和希尔排序
13、线段树
14、Trie
15、并查集
16、AVL 树
17、2 - 3 树
18、红黑树
19、哈希表
20、SQRT 分解
21、计数排序
22、LSD 字符串排序算法
23、MSD 字符串排序算法
24、桶排序
25、字符串匹配 Rabin-Karp 算法
26、字符串匹配 KMP 算法
27、随机算法
28、外存算法
29、排序算法总结
30、力扣练习题

liuyubobobo
一些同学能刷动题了,但其实基础不行
快排搞不懂,AVL 树、红黑树搞不明白,这些都是刷题不能带给你的,因为刷题网站完全不考察这些底层算法和数据结构的理解
但在我看来,这些是真正的计算机科学的基础,反而是刷几个 ac 没那么难
我并不认为软件工程师要了解 dp,但是不了解这些基础的算法和数据结构,绝对是计算机技术深入下去的瓶颈

image

一些基础的 API

image

基础

与   &   有 0 则 0,二进制截断
或   |   有 1 则 1,二进制组合
非   !   取反
异或 ^   无进位相加
时间毫秒值:指的是从 1970 年 1 月 1 日 00:00:00 走到此刻的总的毫秒数,1 s = 1000 ms = 1000000 μs = 1000000000 ns

Arrays.fill(cnt, 0);
Arrays.copyOf(srcArr, length); // length 是新数组的长度
Arrays.copyOfRange(srcArr, l, r + 1);

Arrays.stream(arr).max().getAsInt();
Arrays.stream(arr).sum();

System.arraycopy(srcArr, arcArrStartIndex, targetArr, targetArrStartIndex, length);

"--".repeat(Math.max(0, depth));

treeSet.ceiling(num); // >= num 的最小值
treeSet.floor(num);   // <= num 的最大值
treeSet.subSet(min, max);             // min <= num < max
treeSet.subSet(min, true, max, true); // min <= num <= max
list.stream().mapToInt(Integer::intValue).toArray();

优先队列

public interface Queue<E> extends Collection<E> {

    boolean add(E e);

    boolean offer(E e);

    E remove();

    E poll();

    E element();

    E peek();
}
// 添加元素
public boolean add(E e);
public boolean offer(E e);

// 移除堆顶元素
public E remove();
public E poll();

// 查看堆顶元素
public E element();
public E peek();

PriorityQueue<Integer> pq1 = new PriorityQueue<>();                           // 默认是最小堆
PriorityQueue<Integer> pq2 = new PriorityQueue<>(Collections.reverseOrder()); // 修改为最大堆

栈、队列、双端队列

public static void testStack() {
    Stack<Integer> stack = new Stack<>();

    stack.push(1); // 入栈 1
    stack.push(2); // 入栈 2
    stack.push(3); // 入栈 3

    int top = stack.peek();        // 查看栈顶元素, 返回 3
    int popElement = stack.pop();  // 出栈, 返回 3
    boolean empty = stack.empty(); // 查看栈是否为空, 返回 false
    int size = stack.size();       // 查看栈大小, 返回 2
}
public static void testQueue() {
    Queue<Integer> queue = new LinkedList<>();

    queue.offer(1); // 入队 1
    queue.offer(2); // 入队 2
    queue.offer(3); // 入队 3

    int front = queue.peek();          // 查看队首元素, 返回 1
    int dequeueElement = queue.poll(); // 出队, 返回 1
    boolean empty = queue.isEmpty();   // 查看队列是否为空, 返回 false
    int size = queue.size();           // 查看队列大小, 返回 2
}
public static void testDeque1() {
    Deque<Integer> queue = new ArrayDeque<>(); // 基于数组实现
    Deque<Integer> queue = new LinkedList<>(); // 基于链表实现

    queue.addLast(1);  // 从队尾入队 1
    queue.addLast(2);  // 从队尾入队 2
    queue.addLast(3);  // 从队尾入队 3

    queue.addFirst(1); // 从队首入队 1
    queue.addFirst(2); // 从队首入队 2
    queue.addFirst(3); // 从队首入队 3

    int front = queue.peekFirst();          // 查看队首元素, 返回 3
    int dequeueElement = queue.pollFirst(); // 从队首出队, 返回 3

    int top = queue.peekLast();             // 查看队尾元素, 返回 3
    int popElement = queue.pollLast();      // 从队尾出队, 返回 3

    boolean empty = queue.isEmpty();        // 查看队列是否为空, 返回 false
    int size = queue.size();                // 查看队列大小, 返回 4
}

public static void testDeque2() {
    Deque<Integer> deque = new LinkedList<>();

    deque.push(1);
    deque.push(2);
    deque.push(3);

    System.out.println(deque.peek());    // 输出 3

    while (!deque.isEmpty()) {
        System.out.println(deque.pop()); // 输出顺序: 3、2、1
    }
}

1、递归

递归的执行流程:终结点、递归子问题、当前问题、返回

222...  1  343434...
1、终结点(基础问题)
2、递归子问题
3、当前问题(这里解决问题)
4、返回

常见的递归终结点
1、链表的递归一般是 node == null 或者对 index 的范围进行判断
2、数组的递归一般是区间问题 arr[l, r],l >= r 或 l > r  或 r - l <= 15

2、取数字

1234 % 10 = 4;   // 取出最右边的一位
1234 / 10 = 123; // 去除最右边的一位

3、数组

image

// 获取一个 [l ... r] 之间的随机数, 即 [0 ... r - l] + l
// [l ... r] 的长度为 (r - l + 1)
int x = random.nextInt(r - l + 1) + l; // random.nextInt(区间长度) + 左边界
arr[l ... r] 的元素个数是 n

r - l 代表: l 走到 r 需要多少步(答案是 n - 1 步)
n = r - l + 1
n - 1 = r - l

r = l + (n - 1)
l = r - (n - 1)
自底向上归并排序:遍历数组区间,只需要遍历 "右区间的左端点",同时需要注意 "右端点不要越界"

快速排序:注意 p1、p2 的初始位置
1、单路快排:j 代表左括弧(for)
2、双路快排:p1、p2 代表遍历索引(while)
3、三路快排:p1、p2 代表括弧,需要额外的变量 i 遍历(while)

从左往右数第 K 个元素的索引为 K - 1
从右往左数第 K 个元素的索引为 length - K
数组长度为 N
每组元素个数为 B
组数 Bn = N / B + (N % B != 0 ? 1 : 0)

第几组:index / B
第几组的第几个:index % B

4、增删改查

增加:判断索引(是否满了)
删除:判断索引(是否为空)
修改:判断索引(是否为空)
查看:判断索引(是否为空)

5、循环

// i % 3 的结果为 [0, 1, 2]
// i % n 的结果为 [0 ... n - 1]
for (int i = 0; i < 10; i++) {
    if (i % 3 == 2) {
        queue.dequeue();
        System.out.println(queue);
    }
}

6、链表

dummyHead 简化添加和删除的逻辑
递归:先序、后序
链表的遍历:for (Node cur = head; cur != null; cur = cur.next)、while (cur != null)

7、二分

// 如果问题有明显的边界,就可以用二分查找法在这个边界去搜索问题的解

int l;
int r;
int mid = l + (r - l) / 2;     // l <= mid < r, l 与 r 相邻时 mid = l

int l;
int r;
int mid = l + (r - l + 1) / 2; // l < mid <= r, l 与 r 相邻时 mid = r
l = 0, r = data.length - 1
每次循环开始时: data[l]、data[r] 还没看, 因此 l == r 时, 依然要进入循环

l = 0, r = data.length
每次循环开始时: data[l] 还没看, data[r] 可能是解, 因此当 l == r 时, r 就是解

l = -1, r = data.length - 1
每次循环开始时: data[l] 可能是解, data[r] 还没看, 因此当 l == r 时, l 就是解

8、二分搜索树 BST

二分搜索树基于比较,大多递归的终结点是:node == null(类似递归链表没有 index)

深度优先
1、前序遍历:中左右
2、中序遍历:左中右,二分搜索树的中序遍历结果是顺序的
3、后序遍历:左右中,为二分搜索树释放内存
广度优先:层序遍历,更快找到问题的解,常用于算法设计中 - 无权图最短路径

删除节点
1、有左孩子,没右孩子
2、有右孩子,没左孩子
3、左右都有孩子:用待删除节点的后继节点或前驱节点来顶替待删除节点的位置
  【1】前驱:比待删除节点小的最大节点,即待删除节点左子树的最大节点 predecessor
  【2】后继:比待删除节点大的最小节点,即待删除节点右子树的最小节点 successor

9、Set 和 Map

集合和映射,Set 和 Map,基于 BST 和 LinkedList 实现
1、有序 Set 和 Map:基于 BST 实现
2、无序 Set 和 Map:基于 LinkedList 实现,还可以基于 "哈希表" 来实现

需要注意 Map 中有个工具函数 Node getNode

10、堆和优先队列

堆是完全二叉树,我们可以使用数组来存放堆

建堆方式
1、将 n 个元素逐个插⼊到⼀个空堆中,算法复杂度是 O(N * logN)
2、heapify:从最后一个非叶子节点开始,从后向前,倒序进行 siftDown 操作,算法复杂度为 O(n)

Select K 和 Top K 问题
1、快排思想:时间复杂度 O(n),空间复杂度 O(1)
2、优先队列:时间复杂度 O(N * logK),空间复杂度 O(K)

优先队列的优势:具有实时性,不需要一次性知道所有数据(数据流、极大规模数据)

11、Select K 与 Top K 问题

利用快排思想解决 Select K 与 Top K 问题
利用优先队列解决 Select K 与 Top K 问题

image

12、线段树

线段树不是满二叉树,也不是完全二叉树,而是平衡二叉树
但是,我们可以把它看做是满二叉树,这样,就可以用数组来表示线段树

tree[treeIndex] 代表 data[l ... r] 的融合结果,因此 treeIndex、l、r 它们三者是绑定在一起的

13、Trie

image

14、并查集

用来解决连接问题

1、QuickFind、QuickUnion
2、基于 size 的优化:让 "元素个数少的集合" 合并到 "元素个数多的集合" 上,使得合并后的新树,深度尽量不要增加
3、基于 rank 的优化:让 "层数低的集合" 合并到 "层数高的集合" 上,使得合并后的新树,深度尽量不要增加
4、路径压缩:压缩树的高度使其尽可能的矮

15、AVL 树

Node 新添加了一个成员变量 height

辅助函数
1、获得节点 node 的高度
2、获得节点 node 的平衡因子
3、判断二叉树是否是一棵二分搜索树:中序遍历
4、判断二叉树是否是一棵平衡二叉树:前序遍历

维护自平衡:LL、RR、LR、RL、L、R
1、左旋转,旋转后要更新 height(必须先更新 y, 后更新 x)
2、右旋转,旋转后要更新 height(必须先更新 y, 后更新 x)

在添加和删除时保持自平衡
1、更新 height
2、计算平衡因子 balanceFactor
3、维护自平衡

删除节点后保持自平衡的坑
1、用待删除节点的后继节点或前驱节点来顶替待删除节点的位置时,也需要维护自平衡
2、删除叶子节点后, 返回的 retNode 为 null

16、2 - 3 树

2 - 3 树
添加元素不会添加到空节点
一定是添加到最后搜索到的叶子节点,与它做融合

1、如果插入 "二节点",则融合形成 "三节点"
2、如果插入 "三节点",则融合形成 "四节点",再拆解形成 3 个 "二节点"
  【1】如果父节点为 "二节点",则融合形成 "三节点"
  【2】如果父节点为 "三节点",则重复 2

image

17、红黑树

1、每个节点或者是红色的,或者是黑色的
2、根节点是黑色的
3、每一个叶子节点(最后的空节点)是黑色的
4、如果一个节点是红色的,那么他的孩子节点都是黑色的
5、从一个节点到任意叶子节点,经过的黑色节点是一样的

红黑树是保持 "黑平衡" 的二叉树
严格意义上,不是平衡二叉树,最大高度:2 * logN
红黑树添加和删除比 AVL 树快,查询比 AVL 树慢(它比 AVL 树更高)
Node 新添加了一个成员变量 color,默认插入红色节点(新添加的元素一定是红色的)

辅助函数
1、判断节点 node 的颜色
2、保持根节点为黑色节点
3、左旋转
4、右旋转
5、颜色翻转
添加操作
1、保持根节点为黑色节点
2、新添加的元素一定是红色的
3、保持 "黑平衡"

将一个元素插入到一个 "二节点" 中,使其融合形成 "三节点"
1、添加到左边
2、添加到右边:为了保证所有的红色节点都是左倾斜的,在这里需要通过左旋转来调整

将一个元素插入到一个 "三节点" 中,使其变为临时的 "四节点"
再拆解形成 3 个 "二节点",根节点继续向上,与它的父亲节点做融合
1、添加到右边:颜色翻转
2、添加到左边:右旋转、颜色翻转
3、添加到中间:左旋转、右旋转、颜色翻转
1、二分搜索树
对于完全随机的数据,普通的二分搜索树很好用!
缺点:极端情况退化成链表(或者高度不平衡)

2、AVL 树
对于查询较多的使用情况,AVL 树很好用!

3、红⿊树
红黑树牺牲了平衡性(2 * logN 的高度)
统计性能更优(综合增删改查所有的操作)

4、Splay Tree 伸展树
另一种统计性能优秀的树结构
局部性原理:刚被访问的内容下次高概率被再次访问

image

18、哈希表

good hash table primes

哈希函数的设计是很重要的
"键" 通过哈希函数得到的 "索引" 分布越均匀越好

通用的哈希函数,都是转成整型处理,但它并不是唯一的方法!

原则
1、一致性:如果 a == b,则 hash(a) == hash(b)
2、高效性:计算高效简便
3、均匀性:哈希值均匀分布

int hash = 0;
for (int i = 0; i < s.length(); i++) hash = (hash * B + s.charAt(i)) % M; // B 代表进制, M 代表素数

19、快速幂运算

更多关于快速幂运算

public class Power {

    private Power() {
    }

    /**
     * 递归快速幂
     */
    public static long pow1(long a, int n) {
        if (n == 0) return 1;

        long temp = pow1(a, n / 2);

        if (n % 2 == 1) return temp * temp * a;
        return temp * temp;
    }

    /**
     * 非递归快速幂
     */
    public static long pow2(long a, int n) {
        long res = 1;

        while (n > 0) {
            if ((n & 1) == 1) res *= a;
            n >>= 1;
            a *= a;
        }

        return res;
    }
}
public class Power {

    private static final long MOD = (long) (1e9 + 7);

    private Power() {
    }

    /**
     * 递归快速幂
     */
    public static long pow1(long a, int n) {
        if (n == 0) return 1;

        long temp = pow1(a, n / 2) % MOD;

        if (n % 2 == 1) return temp * temp * a % MOD;
        return temp * temp % MOD;
    }

    /**
     * 非递归快速幂
     */
    public static long pow2(long a, int n) {
        long res = 1;

        while (n > 0) {
            if ((n & 1) == 1) res = res * a % MOD;
            n >>= 1;
            a = a * a % MOD;
        }

        return res;
    }
}

20、各种树

image

满二叉树
- 节点总个数         (2 ^ h) - 1
- 叶子节点的个数      2 ^ (h - 1)
- 非叶子节点个数      2 ^ (h - 1) - 1
posted @ 2023-04-11 15:58  lidongdongdong~  阅读(248)  评论(0编辑  收藏  举报