算法基础03-树、二叉树、二叉搜索树、图、堆、递归、分治、回溯

383 阅读4分钟

树是一个由根和子树构成的二维数据结构,满足以下几个特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;
  • 树里面没有环路(cycle)

由于树的每个节点都具有相同的特点,所以跟树相关的问题几乎都能用递归来解决。

二叉树

每个节点最多只有两颗子树的树。

二叉树的遍历

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        dfs(res, root);
        return res;
    }

    void dfs(List<Integer> res, TreeNode root){
        if(root == null) return;
        dfs(res, root.left);
        res.add(root.val); // 根据访问根节点的顺序不同,可以分为前序、中序、后序遍历,这里是中序遍历
        dfs(res, root.right);
    }
}

二叉搜索树(Binary Search Tree)

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

  • 左子树上的所有节点小于根节点
  • 右子树上的所有节点大于根节点
  • 根节点的左右子树也为二叉搜索树

根据这些性质,二叉搜索树最大的特点就是其中序遍历是升序的,另外二叉搜索树插入、删除、访问、搜索时间复杂度都是O(logn)

图是由定点和边构成的二维数据结构,用来表示元素之间的关系。从定义的角度来说,树其实是特殊化的图(无向无环),而链表又是特殊化的树(只有单边子树)。图的遍历方法有广度优先搜索(Breadth First, BFS Search)和深度优先搜索(Depth First Search, DFS)。

堆是一个顶点元素总是小于其子节点元素的数据结构,所以堆的堆顶元素总是元素列表中最大值或最小值。

二叉堆

二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。因为这些性质,二叉堆的插入、删除、搜索、访问任意元素的操作时间复杂度都是O(logn),访问最大或最小元素的时间复杂度为O(1)。

递归

递归,讲起来很简单,先递进再回归。

Lisp递归

我认为递归是计算机计算效率远超人类的三大原因之一(判断、循环和递归)。因为相比计算机,世世代代人类的老祖先们却只能顺序迭代,所以这就导致了递归是非常反人类天性的,这也是大部分人觉得递归难的根本原因。

递归模板

这里给出递归的Python代码模板,其他语言类似。

def recursion(level, param1, param2...):
    # 1.terminator
    if level > MAX_LEVEL:
        process_result
        return
    # 2.process
    process(level, data...)
    # 3.drill down
    recursion(level+1, p1, p2...)
    # 4.restore the current level's states if needed

递归的要点

  1. 不要一层一层人肉递归,因为人肉递归会很耗脑力,让你非常累(反直觉,但没有办法)
  2. 找到最近重复子问题,或者将一个大问题拆成可重复的子问题
  3. 数学归纳法思维

分治

分治的思想就是把大问题分成小问题,解决小问题,再把小问题的解答合并得到大问题的解答

所以分治的代码模板就是在递归的基础上增加了split the problem和merge subproblems

def divide_conquer(problem, param1, param2, ...):
    # recursion terminator
    if problem is None:
        print_result
        return
    # prepare data
    data = prepare_data(problem)
    subproblems = split_problem(problem, data)
    # conquer subproblems
    subresult1 = self.divide_conquer(subproblems[0], p1, ...)
    subresult2 = self.divide_conquer(subproblems[1], p1, ...)
    subresult3 = self.divide_conquer(subproblems[2], p1, ...)
    …
    # process and generate the final result
    result = process_result(subresult1, subresult2, subresult3, …)
    # revert the current level states

回溯

回溯的思想就是在递归解决问题的过程中,遇到不满足条件的情况,返回上一层重新选择路径解决。

回溯的代码模板是在递归的基础上,加入了对选择列表的循环,最重要的是restore current states,这一步也是回溯的精髓所在。

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        # 做选择
        路径.add(选择)
        (将该选择从选择列表移除)
        backtrack(路径, 选择列表)
        # 撤销选择,restore current states
        路径.remove(选择)
        (将该选择重新添加到选择列表)