算法1

算法

  1. 定义:明确的输入输出,可行的步骤
  2. 循环:
  3. 递归:可借助栈转换为循环,不过代价需要权衡;(尾递归某些编译器可直接优化为循环)
  4. 时间复杂度:运行时间的增长趋势,一般优先时间(明显空间不足时才考虑空间),
    常数阶O(1)<对数阶O(log n)<线性阶O(n)<线性对数阶O(nlog n)<平方阶O(n^2)<指数阶O(2^n)
  5. 空间复杂度:占用内存空间的增长趋势

数组和链表

数组:array

数组的改进-vector/ArrayList动态数组,deque/SegmentedList类似半个deque

链表:list/DoublyLinkedList,forward_list/SinglyLinkedList

链表的改进-跳表

xxx

数组链表的综合改进-哈希表(散列表)

通过建立键key与值value之间的映射,实现高效的元素查询.在哈希表中进行增删改查的时间复杂度都是O(1),非常高效.

负载因子load factor

哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件.

哈希冲突

通常情况下哈希函数的输入空间远大于输出空间,因此理论上哈希冲突是不可避免的.
如f(x)=x % 100中137和237就冲突了.

哈希算法

效率高和均匀分布

查找

二分查找:自适应搜索

/* 二分查找(左闭右开区间) */
int binarySearchLCRO(vector<int> &nums, int target) {
    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
    int i = 0, j = nums.size();
    // 循环,当搜索区间为空时跳出(当 i = j 时为空)
    while (i < j) {
        int m = i + (j - i) / 2; // 计算中点索引 m
        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j) 中
            i = m + 1;
        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
            j = m;
        else // 找到目标元素,返回其索引
            return m;
    }
    // 未找到目标元素,返回 -1
    return -1;
}

树查找

例如二叉搜索树

哈希查找

散列表通过散列函数查找

线性搜索:暴力搜索(遍历)

深度优先搜索

广度优先搜索

排序

排序算法平均复杂度最好情况最坏情况空间复杂度排序方式稳定性
插入排序O(n^2)O(n)O(n^2)O(1)in-place稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)in-place不稳定
冒泡排序O(n^2)O(n)O(n^2)O(1)in-place稳定
希尔排序---O(1)in-place不稳定
堆排序O(nlog n)O(nlog n)O(nlog n)O(1)in-place不稳定
快速排序O(nlog n)O(nlog n)O(n^2)O(log n)in-place不稳定
归并排序O(nlog n)O(nlog n)O(nlog n)O(n)out-place稳定
计数排序O(n+k)O(n+k)O(n+k)O(k)out-place稳定
基数排序O(n*k)O(n*k)O(n*k)O(n+k)out-place稳定
桶排序O(n+k)O(n+k)O(n^2)O(n+k)out-place稳定

插入排序(n~n^2~~n^2 stable)

/* 插入排序 */
void insertionSort(int nums[], int size) {
    // 外循环:已排序元素数量为 1, 2, ..., n
    for (int i = 1; i < size; i++) {
        int base = nums[i], j = i - 1;
        // 内循环:将 base 插入到已排序部分的正确位置
        while (j >= 0 && nums[j] > base) {
            // 将 nums[j] 向右移动一位
            nums[j + 1] = nums[j];
            j--;
        }
        // 将 base 赋值到正确位置
        nums[j + 1] = base;
    }
}

选择排序

冒泡排序

希尔排序

堆排序3(nlogn~nlogn~~nlogn unstable)

/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
void siftDown(vector<int> &nums, int n, int i) {
    while (true) {
        // 判断节点 i, l, r 中值最大的节点,记为 ma
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        int ma = i;
        if (l < n && nums[l] > nums[ma])
            ma = l;
        if (r < n && nums[r] > nums[ma])
            ma = r;
        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
        if (ma == i) {
            break;
        }
        // 交换两节点
        swap(nums[i], nums[ma]);
        // 循环向下堆化
        i = ma;
    }
}

/* 堆排序 */
void heapSort(vector<int> &nums) {
    // 建堆操作:堆化除叶节点以外的其他所有节点
    for (int i = nums.size() / 2 - 1; i >= 0; --i) {
        siftDown(nums, nums.size(), i);
    }
    // 从堆中提取最大元素,循环 n-1 轮
    for (int i = nums.size() - 1; i > 0; --i) {
        // 交换根节点与最右叶节点(交换首元素与尾元素)
        swap(nums[0], nums[i]);
        // 以根节点为起点,从顶至底进行堆化
        siftDown(nums, i, 0);
    }
}

快速排序1(nlogn~n^2~~nlogn unstable)

递归版改为非递归和迭代版:将信息push和pop到栈结构中或者存到范围数组std::pair ranges[len];
内省排序(introsort):快排递归深度达到阈值,退化为O(n^2),此时调整为堆排序,从而将最坏情况优化为nlogn;当元素数低于某个阈值,切换为插入排序
pdqsort:introsort的改进版

/* 选取三个元素的中位数 */
int medianThree(vector<int> &nums, int left, int mid, int right) {
    // 此处使用异或运算来简化代码
    // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1
    if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right]))
        return left;
    else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right]))
        return mid;
    else
        return right;
}

/* 哨兵划分(三数取中值) */
int partition(vector<int> &nums, int left, int right) {
    // 选取三个候选元素的中位数
    int med = medianThree(nums, left, (left + right) / 2, right);
    // 将中位数交换至数组最左端
    swap(nums, left, med);
    // 以 nums[left] 为基准数
    int i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= nums[left])
            j--; // 从右向左找首个小于基准数的元素
        while (i < j && nums[i] <= nums[left])
            i++;          // 从左向右找首个大于基准数的元素
        swap(nums, i, j); // 交换这两个元素
    }
    swap(nums, i, left); // 将基准数交换至两子数组的分界线
    return i;            // 返回基准数的索引
}

/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
    // 子数组长度为 1 时终止递归
    if (left >= right)
        return;
    // 哨兵划分
    int pivot = partition(nums, left, right);
    // 递归左子数组、右子数组
    quickSort(nums, left, pivot - 1);
    quickSort(nums, pivot + 1, right);
}

/* 快速排序(尾递归优化) */
void quickSort(vector<int> &nums, int left, int right) {
    // 子数组长度为 1 时终止
    while (left < right) {
        // 哨兵划分操作
        int pivot = partition(nums, left, right);
        // 对两个子数组中较短的那个执行快速排序
        if (pivot - left < right - pivot) {
            quickSort(nums, left, pivot - 1); // 递归排序左子数组
            left = pivot + 1;                 // 剩余未排序区间为 [pivot + 1, right]
        } else {
            quickSort(nums, pivot + 1, right); // 递归排序右子数组
            right = pivot - 1;                 // 剩余未排序区间为 [left, pivot - 1]
        }
    }
}

归并排序2(nlogn~nlogn~~nlogn stable)

块排序(block sort):混合插入和归并的排序

/* 合并左子数组和右子数组 */
void merge(vector<int> &nums, int left, int mid, int right) {
    // 左子数组区间 [left, mid], 右子数组区间 [mid+1, right]
    // 创建一个临时数组 tmp ,用于存放合并后的结果
    vector<int> tmp(right - left + 1);
    // 初始化左子数组和右子数组的起始索引
    int i = left, j = mid + 1, k = 0;
    // 当左右子数组都还有元素时,比较并将较小的元素复制到临时数组中
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 将左子数组和右子数组的剩余元素复制到临时数组中
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间
    for (k = 0; k < tmp.size(); k++) {
        nums[left + k] = tmp[k];
    }
}

/* 归并排序 */
void mergeSort(vector<int> &nums, int left, int right) {
    // 终止条件
    if (left >= right)
        return; // 当子数组长度为 1 时终止递归
    // 划分阶段
    int mid = (left + right) / 2;    // 计算中点
    mergeSort(nums, left, mid);      // 递归左子数组
    mergeSort(nums, mid + 1, right); // 递归右子数组
    // 合并阶段
    merge(nums, left, mid, right);
}

计数排序

计数排序是桶排序在整型数据下的一个特例

基数排序

桶排序

/* 桶排序 */
void bucketSort(vector<float> &nums) {
    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
    int k = nums.size() / 2;
    vector<vector<float>> buckets(k);
    // 1. 将数组元素分配到各个桶中
    for (float num : nums) {
        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
        int i = num * k;
        // 将 num 添加进桶 bucket_idx
        buckets[i].push_back(num);
    }
    // 2. 对各个桶执行排序
    for (vector<float> &bucket : buckets) {
        // 使用内置稳定排序函数,也可以替换成其他稳定排序算法
        stable_sort(bucket.begin(), bucket.end());
    }
    // 3. 遍历桶合并结果
    int i = 0;
    for (vector<float> &bucket : buckets) {
        for (float num : bucket) {
            nums[i++] = num;
        }
    }
}

二叉树的遍历与表示

广度优先搜索:层序遍历

/* 层序遍历 */
vector<int> levelOrder(TreeNode *root) {
    // 初始化队列,加入根节点
    queue<TreeNode *> queue;
    queue.push(root);
    // 初始化一个列表,用于保存遍历序列
    vector<int> vec;
    while (!queue.empty()) {
        TreeNode *node = queue.front();
        queue.pop();              // 队列出队
        vec.push_back(node->val); // 保存节点值
        if (node->left != nullptr)
            queue.push(node->left); // 左子节点入队
        if (node->right != nullptr)
            queue.push(node->right); // 右子节点入队
    }
    return vec;
}

深度优先搜索:前序,中序,后序遍历;均为O(n)时间复杂度

/* 前序遍历 */
void preOrder(TreeNode *root) {
    if (root == nullptr)
        return;
    // 访问优先级:根节点 -> 左子树 -> 右子树
    vec.push_back(root->val);
    preOrder(root->left);
    preOrder(root->right);
}

/* 中序遍历 */
void inOrder(TreeNode *root) {
    if (root == nullptr)
        return;
    // 访问优先级:左子树 -> 根节点 -> 右子树
    inOrder(root->left);
    vec.push_back(root->val);
    inOrder(root->right);
}

/* 后序遍历 */
void postOrder(TreeNode *root) {
    if (root == nullptr)
        return;
    // 访问优先级:左子树 -> 右子树 -> 根节点
    postOrder(root->left);
    postOrder(root->right);
    vec.push_back(root->val);
}

表示:

二叉搜索树:增删查时间复杂度均为O(log n),但存在退化为链表风险,此时O(n)

  1. 对于根节点,左子树中所有节点的值<根节点的值<右子树中所有节点的值
  2. 任意节点的左,右子树也是二叉搜索树

中序遍历有序:二叉搜索树的中序遍历序列是升序的

AVL树:平衡二叉搜索树

红黑树

红黑树是一种自平衡二叉查找树,具有良好的效率,可以在O(log n)的时间复杂度下完成增删改查.
特性:

b树(b-树)

b+树

b*树


分类:

图的表示:

图的遍历:

/* 广度优先遍历 BFS */
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
    // 顶点遍历序列
    vector<Vertex *> res;
    // 哈希表,用于记录已被访问过的顶点
    unordered_set<Vertex *> visited = {startVet};
    // 队列用于实现 BFS
    queue<Vertex *> que;
    que.push(startVet);
    // 以顶点 vet 为起点,循环直至访问完所有顶点
    while (!que.empty()) {
        Vertex *vet = que.front();
        que.pop();          // 队首顶点出队
        res.push_back(vet); // 记录访问顶点
        // 遍历该顶点的所有邻接顶点
        for (auto adjVet : graph.adjList[vet]) {
            if (visited.count(adjVet))
                continue;            // 跳过已被访问的顶点
            que.push(adjVet);        // 只入队未访问的顶点
            visited.emplace(adjVet); // 标记该顶点已被访问
        }
    }
    // 返回顶点遍历序列
    return res;
}
/* 深度优先遍历 DFS 辅助函数 */
void dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {
    res.push_back(vet);   // 记录访问顶点
    visited.emplace(vet); // 标记该顶点已被访问
    // 遍历该顶点的所有邻接顶点
    for (Vertex *adjVet : graph.adjList[vet]) {
        if (visited.count(adjVet))
            continue; // 跳过已被访问的顶点
        // 递归访问邻接顶点
        dfs(graph, visited, res, adjVet);
    }
}

/* 深度优先遍历 DFS */
// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
    // 顶点遍历序列
    vector<Vertex *> res;
    // 哈希表,用于记录已被访问过的顶点
    unordered_set<Vertex *> visited;
    dfs(graph, visited, res, startVet);
    return res;
}

分治之后是算法第二阶段,需要更高深的理解力和想象力

分治

分而治之揭示了一个重要的事实:从简单做起,一切都不再复杂.
分治算法递归地将原问题划分为多个相互独立的子问题,直至最小子问题,并在回溯中合并子问题的解,最终得到原问题的解.
应用:

/* 移动一个圆盘 */
void move(vector<int> &src, vector<int> &tar) {
    // 从 src 顶部拿出一个圆盘
    int pan = src.back();
    src.pop_back();
    // 将圆盘放入 tar 顶部
    tar.push_back(pan);
}

/* 求解汉诺塔问题 f(i) */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
    // 若 src 只剩下一个圆盘,则直接将其移到 tar
    if (i == 1) {
        move(src, tar);
        return;
    }
    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf
    dfs(i - 1, src, tar, buf);
    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar
    move(src, tar);
    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar
    dfs(i - 1, buf, src, tar);
}

/* 求解汉诺塔问题 */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
    int n = A.size();
    // 将 A 顶部 n 个圆盘借助 B 移到 C
    dfs(n, A, B, C);
}

回溯

回溯的力量让我们能够重新开始,不断尝试,最终找到通往光明的出口.
回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支.
回溯算法通常采用”深度优先搜索”来遍历解空间

  1. 全排列问题:暂略
/* 回溯算法:全排列 I */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
    // 当状态长度等于元素数量时,记录解
    if (state.size() == choices.size()) {
        res.push_back(state);
        return;
    }
    // 遍历所有选择
    for (int i = 0; i < choices.size(); i++) {
        int choice = choices[i];
        // 剪枝:不允许重复选择元素
        if (!selected[i]) {
            // 尝试:做出选择,更新状态
            selected[i] = true;
            state.push_back(choice);
            // 进行下一轮选择
            backtrack(state, choices, selected, res);
            // 回退:撤销选择,恢复到之前的状态
            selected[i] = false;
            state.pop_back();
        }
    }
}

/* 全排列 I */
vector<vector<int>> permutationsI(vector<int> nums) {
    vector<int> state;
    vector<bool> selected(nums.size(), false);
    vector<vector<int>> res;
    backtrack(state, nums, selected, res);
    return res;
}
  1. 子集和问题:暂略

  2. N皇后问题:较复杂

动态规划:满足某种数列公式(状态转换方程)

动态规划也对问题进行递归分解,但与分治算法的主要区别是,动态规划中的子问题是相互依赖的,在分解过程中会出现许多重叠子问题;
动态规划常用来求解最优化问题,特性:

  1. 初探:状态转移方程
//给定一个共有n阶的楼梯,你每步可以上1阶或者2阶,请问有多少种方案可以爬到楼顶?
/* 爬楼梯:动态规划 */
int climbingStairsDP(int n) {
    if (n == 1 || n == 2)
        return n;
    // 初始化 dp 表,用于存储子问题的解
    vector<int> dp(n + 1);
    // 初始状态:预设最小子问题的解
    dp[1] = 1;
    dp[2] = 2;
    // 状态转移:从较小子问题逐步求解较大子问题
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];//状态转移方程
    }
    return dp[n];
}

/* 爬楼梯:空间优化后的动态规划 */
int climbingStairsDPComp(int n) {
    if (n == 1 || n == 2)
        return n;
    int a = 1, b = 2;
    for (int i = 3; i <= n; i++) {
        int tmp = b;
        b = a + b;
        a = tmp;
    }
    return b;
}
  1. 0-1背包问题
//给定n个物品,第i个物品的重量为wgt[i],价值为val[i],和一个容量为cap的背包.每个物品只能选择一次,问在限定背包容量下能放入物品的最大价值
/* 0-1 背包:空间优化后的动态规划 */
int knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {
    int n = wgt.size();
    // 初始化 dp 表
    vector<int> dp(cap + 1, 0);
    // 状态转移
    for (int i = 0; i < n; i++) {
        // 倒序遍历
        for (int c = cap; c >= 1; c--) {
            if (wgt[i] <= c) {
                // 不选和选物品 i 这两种方案的较大值
                dp[c] = max(dp[c], dp[c - wgt[i]] + val[i]);
            }
        }
    }
    return dp[cap];
}
  1. 完全背包:(特例零钱兑换)物品可重复选取,不再仅1次或0次
//给定n个物品,第i个物品的重量为wgt[i-1],价值为val[i-1],和一个容量为cap的背包.每个物品可以重复选取,问在限定背包容量下能放入物品的最大价值.
/* 完全背包:空间优化后的动态规划 */

//给定n中硬币,第i中硬币的面值为coins[i-1],目标金额为amt,每种硬币可以重复选取,问能否凑出目标金额的最少硬币数量.如果无法凑出目标金额,则返回-1.

//给定n中硬币,第i中硬币的面值为coins[i-1],目标金额为amt,每种硬币可以重复选取,问凑出目标金额的硬币组合数量.

贪心算法:值得一试的捷径,有点魏延子午谷奇谋的意思

贪心地做出局部最优的决策,以期获得全局最优解(近似算法捷径);不同于动态规划会根据之前阶段的所有决策来考虑当前决策(用过去子问题解构建当前子问题的解)
正确条件:每一步的最优解一定包含上一步的最优解

  1. 零钱兑换:贪心 (精心设计的钱币对贪心算法有适配,否则动态规划才能达到最优)
/* 零钱兑换:贪心 */
int coinChangeGreedy(int *coins, int size, int amt) {
    // 假设 coins 列表有序
    int i = size - 1;
    int count = 0;
    // 循环进行贪心选择,直到无剩余金额
    while (amt > 0) {
        // 找到小于且最接近剩余金额的硬币
        while (i > 0 && coins[i] > amt) {
            i--;
        }
        // 选择 coins[i]
        amt -= coins[i];
        count++;
    }
    // 若未找到可行方案,则返回 -1
    return amt == 0 ? count : -1;
}
  1. 分数背包问题