阅读 2044

算法基本知识

学习算法的方法

  • 三分学习,七分练习
  • 知识点难点要反复学,直到学会为止,稳扎稳打,切忌看一遍书、视频等完事。
  • 摒弃就习惯,职业化训练
  • 看国际版高手代码,反复练。

leetcode单个题目的解题步骤

  • 读题,一定要弄懂题目的意思
  • 尽可能多的思考解题思路,并列出时空复杂度。
  • 写代码
  • 测试

leetcode五遍刷题方法

  • 切忌死磕,5分钟不会,立刻看题解。
  • 第一遍,看题解,比较不同解法的优劣,背诵优秀题解。
  • 第二遍,不看题解,自己写;每种题解都练习;看高手代码
  • 第三遍,24小时
  • 第四遍,一周
  • 第五遍,面试前

如何精通一个领域

  • 切碎知识点,量化,知识脉络
    • 数据结构 脉络
    • 算法 脉络
  • 刻意练习
    • 一个点,一道题反复练习,过遍数。
  • 获得反馈
    • 主动反馈,看高手代码
    • 被动反馈,高手指点

数据结构

  • 一维:
    • 基础:数组 array (string), 链表 linked list
    • 高级:栈 stack, 队列 queue, 双端队列 deque, 集合 set, 映射 map (hash or map), etc
  • 二维:
    • 基础:树 tree, 图 graph
    • 高级:二叉搜索树 binary search tree (red-black tree, AVL), 堆 heap, 并查集 disjoint set, 字典树 Trie, etc
  • 特殊:
    • 位运算 Bitwise, 布隆过滤器 BloomFilter • LRU Cache

算法

  • leetcode几乎所有题最终都是找其重复单元,并使用以下三种去解决
    • If-else, switch —> branch
    • for, while loop —> Iteration
    • 递归 Recursion (Divide & Conquer, Backtrace)
  • 泛化的高级算法
    • 搜索 Search: 深度优先搜索 Depth first search, 广度优先搜索 Breadth first search, A*, etc
    • 动态规划 Dynamic Programming
    • 二分查找 Binary Search
    • 贪心 Greedy
    • 数学 Math , 几何 Geometry

自顶向下的编程方式

  • clean code
  • 高层次主干逻辑,一起,在上面,函数细节在下面。

工欲善其事必先利其器

  • androidstudio + vscode
  • ide的使用技巧、快捷键

第一周

时间复杂度

  • 如何理解算法时间复杂度的表示法
  • 语句执行的遍数与n的关系
  • 递归怎么求时间复杂度?
    - 画出递归树
    复制代码
    - [主定理](https://zh.wikipedia.org/wiki/%E4%B8%BB%E5%AE%9A%E7%90%86)
    复制代码
        - 二分查找,O(logn),一半,一半,一半
        - 二叉树遍历,O(n),所有节点查一遍,n
        - 二维有序矩阵查找,O(n)
        - 归并排序,O(nlogn)
    复制代码

空间复杂度

  • 数组的长度
  • 递归的深度
  • 如果递归中有数组,那么比较递归深度与数组,取最大的那个。

数组

原理:创建一个数组时,计算机内存管理器会开辟一段连续的地址,每一个地址都可以通过内存管理器直接访问。

因此对于数组

  • 访问单个元素的时间复杂度是O(1)的。
  • 删除一个元素,要把该元素后面所有元素前移,时间复杂度,最坏O(n),最好O(1)。
  • 插入一个元素,要把该元素后面所有元素后移,时间复杂度,最坏O(n),最好O(1)。

ArrayList的java实现

链表

单链表,实现

双向链表 因此对于链表

  • 插入和删除节点,时间复杂度,O(1)
  • 查询节点,时间复杂度,最好O(1),最坏O(n)

相关题目:

跳表 skip list

对于有序链表,考虑“有序”的特性,设计一个数据结构,降低其查询的时间复杂度,即跳表。跳表对标的是平衡树和二分查找,是一种插入、删除、搜索都是logn的数据结构。

栈Stack

先入后出;添加、删除皆为 O(1) java中的stack源码

java中的stack的使用:

Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack);
System.out.println(stack.search(4));
stack.pop();
stack.pop();
Integer topElement = stack.peek();
System.out.println(topElement);
System.out.println(" 3的位置 " + stack.search(3));
复制代码

什么题目用栈解决:

  • 最近相关性的题目
  • 柱状图求最大面积问题

队列 Queue

先入先出;添加、删除皆为 O(1) java中的queue源码

java中的queue使用:

Queue<String> queue = new LinkedList<String>();
queue.offer("one");
queue.offer("two");
queue.offer("three");
queue.offer("four");
System.out.println(queue);
String polledElement = queue.poll();
System.out.println(polledElement);
System.out.println(queue);
String peekedElement = queue.peek();
System.out.println(peekedElement);
System.out.println(queue);
while(queue.size() > 0) {
  System.out.println(queue.poll());
}
复制代码

双端队列Deque

double ended queue,队列两端都可插入删除,插入和删除都是 O(1) 操作

java中的deque:

Deque<String> deque = new LinkedList<String>();
deque.push("a");
deque.push("b");
deque.push("c");
System.out.println(deque);
String str = deque.peek();
System.out.println(str);
System.out.println(deque);
while (deque.size() > 0) {
  System.out.println(deque.pop());
}
System.out.println(deque);
复制代码

题目:

  • 滑动窗口问题

优先队列

保持了一定的有序性,取出元素按优先级取出

  • 插入操作:O(1)
  • 取出操作:O(logN) - 按照元素的优先级取出
  • 底层具体实现的数据结构较为多样和复杂:heap、bst、treap
  • Java 的 PriorityQueue

第二周

哈希表,映射,集合

哈希表(Hash table),也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。 它通过把关键码值映射到表中一个位置来访问记录,以加快查找的 速度。 这个映射函数叫作散列函数(Hash Function),存放记录的数组 叫作哈希表(或散列表)。

哈希函数,计算哈希值的函数,尽可能分散,保持唯一

哈希冲突,如果哈希函数的值有重复,增维,用一个链表解决

完整结构

树、二叉树、二叉搜索树

链表、树、图之间关系

单链表

二叉树

特性:

  • LinkedList 是特殊化的 Tree,每个节点只有一个子节点
  • 二叉树是特殊化的Tree,每个节点最多有两个子节点。
  • Tree 是特殊化的 Graph,无环的图,即树。

二叉树

二叉树的遍历:

  • 前序(Pre-order):根-左-右
  • 中序(In-order):左-根-右
  • 后序(Post-order):左-右-根

java代码,树的节点定义:

public class TreeNode {
  public int val;
  public TreeNode left, right;
  public TreeNode(int val) {
   this.val = val;
   this.left = null;
   this.right = null;
  } 
}
复制代码

二叉搜索树 Binary Search Tree

二叉搜索树,也称二叉搜索树、有序二叉树(Ordered Binary Tree)、 排序二叉树(Sorted Binary Tree),是指一棵空树或者具有下列性质的 二叉树:

  1. 左子树上所有结点的值均小于它的根结点的值;
  2. 右子树上所有结点的值均大于它的根结点的值;
  3. 以此类推:左、右子树也分别为二叉查找树。 (这就是 重复性!)

特性:

  • 中序遍历,为升序排列,一维有序数组。
  • 对比二分查找
  • 插入、删除、查找,时间复杂度:O(logn)

动态演示二叉搜索树的插入、删除等 树的遍历 Demo

Heap:可以迅速从一堆数中找出最大值或最小值的数据结构。

将根节点最大的堆叫大顶堆或大根堆,将根节点最小的堆叫小顶堆或小根堆。

时间复杂度,以大顶堆为例:

  • 查最大值,O(1)
  • 删除最大值,O(logn)
  • 插入一个值,非叶子节点O(logn) ,叶子节点,O(1)

二叉多有很多实现方式,常见的堆有二叉堆,斐波那契堆。

二叉堆

通过完全二叉树来实现。

HeapSort :自学

完全二叉树:

  • 除了叶子节点,所有的节点都有两个节点。
  • 叶子节点从左到右依次存在,不能存在空位。

二叉堆(大顶堆):

  • 是一棵完全树
  • 树中任意节点的值总是>=其子节点的值

二叉堆的实现细节:

  • 一般是通过数组来存储,如上图[110,100,90,40,80,20,60,10,30,50,70]
  • 假设第一个节点在数组中索引为0的话,其父子节点关系如下:
    • 索引为i的节点,其左孩子的索引为,2i+1
    • 索引为i的节点,其右孩子的索引为,2i+2
    • 索引为i的节点,其父节点的索引为,floor((i-1)/2)

二叉堆的插入,O(logn):

  • 新元素一律插到堆的尾部。如上,也就是70的后面。
  • 依次向上调整整个堆的结构,直到符合二叉堆的特性。

删除二叉堆顶的操作,O(logn):

  • 删除堆顶元素,用堆尾节点接替自己。
  • 从顶部向下,依次调整堆的结构,直到符合二叉堆的特性。

实际应用:

  • PriorityQueue优先队列,采用二叉堆实现。
  • 一般是取前几个xxx的类型的题目。如一堆数中取前k个最小的,频率最高的k个等。
  • 滑动窗口问题等

什么是图?

图的表示:

  • Graph(V,E)
  • V - vertex:点
    • 度,入度和出度。度,该顶点相连的边的数量;入度,有几条边是指向该顶点的;出度,有几条边是指向其他顶点的
      • 点与点之间:连通与否
  • E - edge:边
    - 有向和无向(单行线)
    - 权重(边长)
    复制代码
    图的表示,实现:
  • 邻接矩阵,
  • 邻接表

图的分类:

图的常见算法

  • DFS
  • BFS

参考学习:

第三周

递归

递归:

  • 本质是循环
  • 特殊是通过调用自己来实现循环
  • 一层一层下钻,一层一层回来

递归的特性:不重复的遍历所有可能性

写递归的步骤:

  • 定义终止条件
  • 当前层的业务代码
  • 去往下一层
  • 进行当前层清理

模板:

public void recur(int level,int param){
    //定义终止条件和返回值
    if(level>MAX_VALUE){
        return;
    }
    //处理当前层逻辑 
    process(level,param);
    
    // 下钻一层
    recur(level+1,newParam);
    
    // 清理当前层
    cleanCur()
}
复制代码

示意图: 思维要点:

  • 不要人肉递归,
  • 找到最近重复子问题
  • 应用数学归纳法

分治、回溯

分治:

  • 也是采用递归
  • 拆分为多个子问题

分治与一般递归的区别:

  • 一般递归是调用一次自己,然后一层一层深入,再一层一层返回。 如:
public void recur(int level,int param){
    ... 
    recur(level+1,newParam); // 一次调用
    ...
}
复制代码
  • 分治则是在一个递归题内,调用了多次自己,每一次调用都是解决 一个子问题。 如:
public int recur(int param){
    ... 
    int result1 = recur(param1) // 子问题1
    int result2 = recur(param2) // 子问题2
    return result1+result2; // 子结果合并
    ...
}
复制代码

分治代码模板

回溯: 核心思想:就是每一层去试。

回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程 中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将 取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问 题的答案。 回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种 情况: • 找到一个可能存在的正确的答案; • 在尝试了所有可能的分步方法后宣告该问题没有答案。 在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

第四周

深度优先搜索和广度优先搜索

树的遍历:

定义 树 的节点 :

Java
public class TreeNode {
  public int val;
  public TreeNode left, right;
  public TreeNode(int val) {
   this.val = val;
   this.left = null;
   this.right = null;
} }
复制代码

树的遍历

  • 其实就是将每个节点都访问一遍
  • 每个节点只访问一次
  • 对于节点的访问,有序顺序如下:
    • 深度优先
    • 广度优先

深度优先搜索:

  • 从根节点开始,从左到右,依次访问边的每个节点。
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> allResults = new ArrayList<>();
        if(root==null){
            return allResults;
        }
        travel(root,0,allResults);
        return allResults;
    }


    private void travel(TreeNode root,int level,List<List<Integer>> results){
        if(results.size()==level){
            results.add(new ArrayList<>());
        }
        results.get(level).add(root.val);
        if(root.left!=null){
            travel(root.left,level+1,results);
        }
        if(root.right!=null){
            travel(root.right,level+1,results);
        }
    }
复制代码

广度优先搜索:

  • 从root开始,一层一层往下遍历,每层从左到右。
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
        val = x;
    }
}

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> allResults = new ArrayList<>();
    if (root == null) {
        return allResults;
    }
    Queue<TreeNode> nodes = new LinkedList<>();
    nodes.add(root);
    while (!nodes.isEmpty()) {
        int size = nodes.size();
        List<Integer> results = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = nodes.poll();
            results.add(node.val);
            if (node.left != null) {
                nodes.add(node.left);
            }
            if (node.right != null) {
                nodes.add(node.right);
            }
        }
        allResults.add(results);
    }
    return allResults;
}
复制代码

贪心算法

  • 贪心算法是一种在 每一步选择中都 采取在当前状态下最好或最优的 选择,从而希望导致结果是全局最好或最优的算法。

  • 贪心算法与动态规划的不同在于,它对每个子问题的解决方案都做出选择,不能回退。动态规划则会 保存以前的运算结果,并根据以前的结果 对当前进行选择,有回退功能。

贪心 成立的例子

在某些条件下,贪心算法是成立的,比如用零钱拼数的问题:

面额: 20 10 5 1 , 用最少的硬币拼出36。

贪心不成立的例子

还是零钱拼凑问题: 面额: 10 9 1 , 用最少的硬币拼出18。很明显,最优解是 9 9 但是贪心算法: 明显不是最优解

适合贪心的情况

  • 贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码 等。然而对于工程和生活中的问题,贪心法一般不能得到我们所要求的答 案。
  • 最优子结构 简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终 问题的最优解。这种子问题最优解称为最优子结构。
  • 一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最 好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心 法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。

举例:分发饼干

  • 对饼干和小孩排序
  • 让饼干刚刚满足小孩(这时就是当前步骤最优解)
  • 持续分发(每一步最优,促成全局最优)

二分查找

前提:

  • 序列,单调递增或递减
  • 存在上下界
  • 能够通过索引访问

代码模板:

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;
}
复制代码

第五周

第六周

动态规划

关键点

  • 动态规划 和 递归或者分治 没有根本上的区别(关键看有无最优的子结构,没有最优子结构,那就是分治;有最优子结构,那就是动态规划)
  • 共性:找到重复子问题
  • 差异性:最优子结构、中途可以淘汰次优解

解决的问题:

  • 将一个复杂的问题分解为简单的子问题
  • 递归分治+最优子结构 动态 规划定义

动态规划关键点

  • 最优子结构 opt[n] = best_of(opt[n-1], opt[n-2], ...)
  • 储存中间状态:opt[i]
  • 递推公式(美其名曰:状态转移方程或者 DP 方程)
    • 自顶向下:Fib: opt[n] = opt[n-1] + opt[n-2]
    • 子底向上:
for(int i=2;i<n;i++)
{
    f(i) = f(i-1)+f(i-2);
}
复制代码
- 二维路径:opt[i,j] = opt[i+1][j] + opt[i][j+1] (且判断a[i,j]是否空地)
复制代码

动态规划的步骤:

  • 分析问题,找出规律,拆解为求子问题。
  • 而这个规律,可能是自顶向下的,也可能是自底向上的,一般来说就是怎么处理f(n-1)与f(n-2)的关系,把这个规律用方法表示出来,也就是形成了dp方程。如f(n) = f(n-1) * f(n-2)
  • 一般我们是来找自底向上的,如果是一维的,那就是找f(0) f(1);如果是二维的,那就是找所有的f(0,y) 和 f(x,0);如果是三维的,那就找的更多了。也就是说,要把动态规划方程的基准点的值先给明确,也就是要把0给明确,而到底要明确几个,就看dp方程的=号有几个状态。而明确基准点的值的过程,是通过肉眼分析规律后,直接写出来的。基准点之上的计算,则是基于基准点来递推出来的。也就是说,先找到0,1,然后根据0,1计算2,然后根据1,2计算3,然后根据2,3计算4.
  • dp的表现形式一般是dp表格,也就是一维数组或二维数组,把过程数据记录下来。一维数组基准值就是,dp[0]=1,dp[1]=2,然后dp[2]=dp[0]+dp[1],dp[3]=dp[2]+dp[1];二维数组基准值就是dp[0,0]=0,dp[0,1]=1,dp[1,0],然后计算dp[1][1]=dp[0][1]+dp[1][0],然后再向上一直递推
  • 从而求解

MIT 动态规划课程最短路径算法

第七周

字典树和并查集

字典树Trie:

基本性质:

  • 结点本身不存完整单词,只有当前位置的字符;
  • 从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的 字符串;
  • 每个结点的所有子结点路径代表的字符都不相同。

字典树模板:

class Trie {
    private boolean isEnd;
    private Trie[] next;
    /** Initialize your data structure here. */
    public Trie() {
        isEnd = false;
        next = new Trie[26];
    }
    
    /** Inserts a word into the trie. */
    public void insert(String word) {
        if (word == null || word.length() == 0) return;
        Trie curr = this;
        char[] words = word.toCharArray();
        for (int i = 0;i < words.length;i++) {
            int n = words[i] - 'a';
            if (curr.next[n] == null) curr.next[n] = new Trie();
            curr = curr.next[n];
        }
        curr.isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        Trie node = searchPrefix(word);
        return node != null && node.isEnd;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        Trie node = searchPrefix(prefix);
        return node != null;
    }

    private Trie searchPrefix(String word) {
        Trie node = this;
        char[] words = word.toCharArray();
        for (int i = 0;i < words.length;i++) {
            node = node.next[words[i] - 'a'];
            if (node == null) return null;
        }
        return node;
    }
}
复制代码

注意:下图中虽然节点的圈内有te、ten、to等,但并不表示该节点存储这些完整单词,只是做了示意。叶子节点下面的数字,表示额外信息,比如15表示访问的频率

字典树的核心思想:

  • 空间换时间,会产生额外巨大的空间,但是将插入、和查询的时间复杂度降低为O(k),k为字符串长度,其实就是O(1)。
  • 利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

并查集disjoint set

并查集的核心思想比喻:

  • 判断两个员工是不是在同一个公司,只要判断两个员工所在公司的ceo是不是同一个人就可以了。

  • 类似的,判断两个元素是不是在一个组、圈内等,只要各自找到所属组的parent,比较是否同一个就行了。

  • 两个组、圈、公司,合并成一个组、圈、公司,那么在新的组、圈、公司中肯定 只有 一个老大,要么是1的,要么是2的。

适用场景:

  • 组团、配对问题,一个人是否在一个组中
  • 组 组 关系

基本操作:

  • makeSet(s):建立一个新的并查集,其中包含s个单元素集合。
  • unionSet(x,y):把元素x和元素y所在的集合 合并,要求x和y所在的集合不相交,如果相交则不合并。
  • find(x):找到元素x所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。

并查集的java实现:

class UnionFind {
  private int count = 0;
  private int[] parent;
  public UnionFind(int n) {
   count = n;
   parent = new int[n];
   for (int i = 0; i < n; i++) {
     parent[i] = i;
   }
  }
  public int find(int p) {
   while (p != parent[p]) {
     parent[p] = parent[parent[p]];
     p = parent[p];
   }
    return p; 
  }
  public void union(int p, int q) {
   int rootP = find(p);
   int rootQ = find(q);
   if (rootP == rootQ) return;
     parent[rootP] = rootQ;
     count--; }
   }
}
复制代码

初级搜索

  • 朴素搜索
  • 优化方式:不重复(fibonacci)、剪枝(生成括号问题)
  • 搜索方向:
    • DFS: depth first search 深度优先搜索 BFS: breadth first - search 广度优先搜索

高级搜索-剪枝

在搜索时,及时判断几点是否符合条件,如不符合,相当于以该节点为root的数枝不再继续搜索,相当于剪枝了。 剪枝,可以理解为及时止损。

回溯法

  • 回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
  • 回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
    • 找到一个可能存在的正确的答案
    • 在尝试了所有可能的分步方法后宣告该问题没有答案
    • 在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

高级搜索-双向BFS

BFS:

双向BFS,字面见其意,从两个方向同时开始进行bfs,最快交接的点为最短路径。

高级搜索-启发式搜索(A*)

Heuristic Search(A*)带有方向性的搜索

  • 基于BFS
  • 在BFS中,将普通的队列,换为优先队列
  • 既然为优先队列,那么肯定要定义怎么“优先”,估价函数(或启发式函数),就是干这个的

启发式函数:h(n),它用来评价哪些节点最优希望的是一个我们要找的节点,h(n)会返回一个非负实数,也可以认为是从节点n的目标节点路径的估计成本。

启发式函数是一种告知搜索方向的方法,它提供了一种明智的方法来猜测哪个邻居节点会导向一个 目标。在向目标前进时,我们应该优先选择哪个方向前进。 代码模板:

public class AStar
    {
        public final static int BAR = 1; // 障碍值
        public final static int PATH = 2; // 路径
        public final static int DIRECT_VALUE = 10; // 横竖移动代价
        public final static int OBLIQUE_VALUE = 14; // 斜移动代价
        
        Queue<Node> openList = new PriorityQueue<Node>(); // 优先队列(升序)
        List<Node> closeList = new ArrayList<Node>();
        
        /**
         * 开始算法
         */
        public void start(MapInfo mapInfo)
        {
            if(mapInfo==null) return;
            // clean
            openList.clear();
            closeList.clear();
            // 开始搜索
            openList.add(mapInfo.start);
            moveNodes(mapInfo);
        }
    

        /**
         * 移动当前结点
         */
        private void moveNodes(MapInfo mapInfo)
        {
            while (!openList.isEmpty())
            {
                Node current = openList.poll();
                closeList.add(current);
                addNeighborNodeInOpen(mapInfo,current);
                if (isCoordInClose(mapInfo.end.coord))
                {
                    drawPath(mapInfo.maps, mapInfo.end);
                    break;
                }
            }
        }
        
        /**
         * 在二维数组中绘制路径
         */
        private void drawPath(int[][] maps, Node end)
        {
            if(end==null||maps==null) return;
            System.out.println("总代价:" + end.G);
            while (end != null)
            {
                Coord c = end.coord;
                maps[c.y][c.x] = PATH;
                end = end.parent;
            }
        }
    

        /**
         * 添加所有邻结点到open表
         */
        private void addNeighborNodeInOpen(MapInfo mapInfo,Node current)
        {
            int x = current.coord.x;
            int y = current.coord.y;
            // 左
            addNeighborNodeInOpen(mapInfo,current, x - 1, y, DIRECT_VALUE);
            // 上
            addNeighborNodeInOpen(mapInfo,current, x, y - 1, DIRECT_VALUE);
            // 右
            addNeighborNodeInOpen(mapInfo,current, x + 1, y, DIRECT_VALUE);
            // 下
            addNeighborNodeInOpen(mapInfo,current, x, y + 1, DIRECT_VALUE);
            // 左上
            addNeighborNodeInOpen(mapInfo,current, x - 1, y - 1, OBLIQUE_VALUE);
            // 右上
            addNeighborNodeInOpen(mapInfo,current, x + 1, y - 1, OBLIQUE_VALUE);
            // 右下
            addNeighborNodeInOpen(mapInfo,current, x + 1, y + 1, OBLIQUE_VALUE);
            // 左下
            addNeighborNodeInOpen(mapInfo,current, x - 1, y + 1, OBLIQUE_VALUE);
        }
    

        /**
         * 添加一个邻结点到open表
         */
        private void addNeighborNodeInOpen(MapInfo mapInfo,Node current, int x, int y, int value)
        {
            if (canAddNodeToOpen(mapInfo,x, y))
            {
                Node end=mapInfo.end;
                Coord coord = new Coord(x, y);
                int G = current.G + value; // 计算邻结点的G值
                Node child = findNodeInOpen(coord);
                if (child == null)
                {
                    int H=calcH(end.coord,coord); // 计算H值
                    if(isEndNode(end.coord,coord))
                    {
                        child=end;
                        child.parent=current;
                        child.G=G;
                        child.H=H;
                    }
                    else
                    {
                        child = new Node(coord, current, G, H);
                    }
                    openList.add(child);
                }
                else if (child.G > G)
                {
                    child.G = G;
                    child.parent = current;
                    openList.add(child);
                }
            }
        }
    

        /**
         * 从Open列表中查找结点
         */
        private Node findNodeInOpen(Coord coord)
        {
            if (coord == null || openList.isEmpty()) return null;
            for (Node node : openList)
            {
                if (node.coord.equals(coord))
                {
                    return node;
                }
            }
            return null;
        }
    

    

        /**
         * 计算H的估值:“曼哈顿”法,坐标分别取差值相加
         */
        private int calcH(Coord end,Coord coord)
        {
            return Math.abs(end.x - coord.x)
                    + Math.abs(end.y - coord.y);
        }
        
        /**
         * 判断结点是否是最终结点
         */
        private boolean isEndNode(Coord end,Coord coord)
        {
            return coord != null && end.equals(coord);
        }
    

        /**
         * 判断结点能否放入Open列表
         */
        private boolean canAddNodeToOpen(MapInfo mapInfo,int x, int y)
        {
            // 是否在地图中
            if (x < 0 || x >= mapInfo.width || y < 0 || y >= mapInfo.hight) return false;
            // 判断是否是不可通过的结点
            if (mapInfo.maps[y][x] == BAR) return false;
            // 判断结点是否存在close表
            if (isCoordInClose(x, y)) return false;
    

            return true;
        }
    

        /**
         * 判断坐标是否在close表中
         */
        private boolean isCoordInClose(Coord coord)
        {
            return coord!=null&&isCoordInClose(coord.x, coord.y);
        }
    

        /**
         * 判断坐标是否在close表中
         */
        private boolean isCoordInClose(int x, int y)
        {
            if (closeList.isEmpty()) return false;
            for (Node node : closeList)
            {
                if (node.coord.x == x && node.coord.y == y)
                {
                    return true;
                }
            }
            return false;
        }
    }
复制代码

红黑树和AVL树

二叉树遍历:

  • 前序(Pre-order):根-左-右
  • 中序(In-order):左-根-右
  • 后序(Post-order):左-右-根

二叉搜索树 Binary Search Tree

二叉搜索树,也称二叉搜索树、有序二叉树(Ordered Binary Tree)、 排序二叉树(Sorted Binary Tree),是指一棵空树或者具有下列性质的 二叉树:

  • 左子树上所有结点的值均小于它的根结点的值;
  • 右子树上所有结点的值均大于它的根结点的值;
  • 以此类推:左、右子树也分别为二叉查找树。 (这就是 重复性!)

中序遍历:升序排列

二分查找:O(logn)

极端情况:O(n)

如何使查找更快:

  • 保证二维维度! —> 左右子树结点平衡(recursively)
  • 尽量均分
  • 点击查看

如何保持平衡? AVL 树

  • 发明者 G. M. Adelson-Velsky 和 Evgenii Landis
  • Balance Factor(平衡因子): 是它的左子树的高度减去它的右子树的高度(有时相反)。 balance factor = {-1, 0, 1}
  • 通过旋转操作来进行平衡(四种)
  • 点击查看

插入14前的平衡因子: 插入14后的平衡因子: 增加3前的平衡因子: 插入3后的平衡因子:

对于不平衡的二叉树,可以通过旋转来改进平衡

  • 左旋
  • 右旋
  • 左右旋
  • 右左旋

左左子树-右旋

右右子树-左旋

左右子树-左右旋 寻找平衡因子是-2的,然后从其子节点开始旋转,如果有-3 -4,都是从-2开始旋

右左子树-右左旋 寻找平衡因子是-2的,然后从其子节点开始旋转,如果有-3 -4,都是从-2开始旋

AVL 总结

  • 平衡二叉搜索树
  • 每个结点存 balance factor = {-1, 0, 1} 3. 四种旋转操作

不足:结点需要存储额外信息、且调整次数频繁

红黑树 Red-black Tree

红黑树是一种近似平衡的二叉搜索树(Binary Search Tree),它能够确保任何一 个结点的左右子树的高度差小于两倍。具体来说,红黑树是满足如下条件的二叉搜 索树:

  • 每个节点要么是红色,要么是黑色
  • 根节点是黑色每个叶节点(NIL节点,空节点)是黑色的。
  • 不能有相邻接的两个红色节点
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。

总结:

  • 二叉搜索树极端情况是一根棍子,这样时间复杂度就是O(n)。
  • 对于二叉搜索树,其时间复杂度,其实就是树的深度。
  • 因此要尽量保持树是均匀分布,这样深度最小,时间复杂度最低。
  • 因此就引出AVL,控制子树左右深度差不大于1.
  • 但是AVL特性,在插入和删除时,要时刻保持二叉树的平衡,操作比较麻烦。AVL是严格平衡的,因此当读多的时候,也就是查询的时候,效率是最高的。但是插入删除,因为一直要调整平衡,因此效率不高。
  • 因此出现了红黑树,将AVL的平衡因子从左右之差为-1,调整为左右高度小于2倍,减少了一些保持平衡的操作。而查询的时间复杂度还保持logn。这时候插入和删除,操作相对较少,因此如果插入和删除较多就用红黑树。

第八周

位运算

  • 位运算符
  • 算数移位与逻辑移位
  • 位运算的应用

为什么需要位运算

  • 机器里的数字表示方式和存储格式就是 二进制
  • 十进制 <—>二进制 : 如何转换?
  • [点击查看](zh.wikihow.com/%E4%BB%8E%E… %E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E8%BF%9B%E5%88% B6)

十进制 转二进制方法

十进制与二进制

  • 4(d): 0100
  • 8(d): 0100
  • 5(d): 0101
  • 6(d): 0110

XOR - 异 或

异或:相同为 0,不同为 1。也可用“不进位加法”来理解。

异或操作的一些特点:

  • x^0=x
  • x^1s=~x //注意1s=~0
  • x ^ (~x) = 1s
  • x^x=0
  • c=a^b => a^c=b,b^c=a //交换两个数
  • a^b^c=a^(b^c)=(a^b)^c // associative

重点看如下:

布隆过滤器 Bloom Filter

Bloom Filter vs Hash Table

  • 一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索 一个元素是否在一个集合中。
  • 优点是空间效率和查询时间都远远超过一般的算法,
  • 缺点是有一定的误识别率和删除困难。

主要作用:快速判断一个xxx如String 是否不存在于哈希表中。仅判断有无。不存储额外信息。实际使用中当做一个快速过滤

举例:如下图 将字符串通过一个哈希函数得出一个数值,该数值为哈希表的key,value为一个链表,用于存储重复元素。如果有多个字符串的哈希值为同一个,那么链表会依次链加存储字符串。 因此,如果一个字符串的哈希值在哈希表中的key中没有找到,那该字符串肯定不存在于该哈希表中。如果一个字符串的哈希值存在于哈希表的key中,那么该字符串有可能存在于该哈希表中,因为存在多字符串哈希值相同情况。

布隆过滤器的原理和实现

使用布隆过滤器解决缓存击穿、垃圾邮件识别、集合判重

布隆过滤器 Java 实现示例 1

布隆过滤器 Java 实现示例 2

实际应用:

LRUCache

LRU:least recently used,最近最少使用

LRU Cache,只缓存最近最少使用的数据。

  • 两个要素: 大小 、替换策略
  • Hash Table + Double LinkedList
  • O(1) 查询
  • O(1) 修改、更新

LRU Cache — Java

public class LRUCache {
     private Map<Integer, Integer> map;
     public LRUCache(int capacity) {
         map = new LinkedCappedHashMap<>(capacity);
     }
     public int get(int key) {
         if(!map.containsKey(key)) { return -1; }
         return map.get(key);
     }
     public void put(int key, int value) {
         map.put(key,value);
     }
     private static class LinkedCappedHashMap<K,V> extends LinkedHashMap<K,V>      {
         int maximumCapacity;
         LinkedCappedHashMap(int maximumCapacity) {
             super(16, 0.75f, true);
             this.maximumCapacity = maximumCapacity;
         }
         protected boolean removeEldestEntry(Map.Entry eldest) {
             return size() > maximumCapacity;
         } 
     }
}

复制代码

排序算法

排序算法

初级排序 - O(n^2)

  • 选择排序(Selection Sort) 每次找最小值,然后放到待排序数组的起始位置。
  • 插入排序(Insertion Sort) 从前到后逐步构建有序序列;对于未排序数据,在已排序序列中从后 向前扫描,找到相应位置并插入。
  • 冒泡排序(Bubble Sort) 嵌套循环,每次查看相邻的元素如果逆序,则交换。

高级排序 - O(N*LogN)

  • 快速排序(Quick Sort) 数组取标杆 pivot,将小元素放 pivot左边,大元素放右侧,然后依次 对右边和右边的子数组继续快排;以达到整个序列有序。
  • 归并排序(Merge Sort)— 分治
    • 把长度为n的输入序列分成两个长度为n/2的子序列; 2. 对这两个子序列分别采用归并排序;
    • 将两个排序好的子序列合并成一个最终的排序序列。
  • 堆排序(Heap Sort) — 堆插入 O(logN),取最大/小值 O(1)
    • 数组元素依次建立小顶堆
    • 依次取堆顶元素,并删除

归并 和 快排 具有相似性,但步骤顺序相反

  • 归并:先排序左右子数组,然后合并两个有序子数组
  • 快排:先调配出左右子数组,然后对于左右子数组进行排序

特殊排序 - O(n)

  • 计数排序(Counting Sort) 计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化为键存 储在额外开辟的数组空间中;然后依次把计数大于 1 的填充回原数组
  • 桶排序(Bucket Sort) 桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有 限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式 继续使用桶排序进行排)。
  • 基数排序(Radix Sort) 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类 推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按 高优先级排序。

第九周

高级动态规划

字符串算法

Boyer-Moore 算法

Sunday 算法

字符串匹配暴力法代码示例

Rabin-Karp 代码示例

KMP 字符串匹配算法视频

字符串匹配的 KMP 算法

各数据结构,时空复杂度对比总图:

常用解题思路

  • 暴力循环
  • 双指针法,左右指针,快慢指针
  • 分析规律,找最近重复子问题,找重复性,泛化,递推,用代码写规则