数据结构与算法
常用排序算法
实现比较丑陋,勿喷啊
冒泡排序:从前向后比较相邻的元素。如果前一个比后一个大,就交换他们两个,每一轮把一个最大的数运到数组最后面。
public static int[] sort(int[] arr) { int len = arr.length; // 冒泡总次数 for (int i = 1; i < len; i++) { boolean flag = true; // 每次冒泡过程 for (int j = 0; j < len - i; j++) { if (arr[j] > arr[j + 1]) { MyUtils.swap(arr, j, j + 1); flag = false; } } if (flag) { // 如果一个冒泡过程没改变,退出返回已经有序 break; } } return arr; }
选择排序:每次从未排序数组中找一个最小的元素,放到以有序数组后面
public static int[] sort(int[] arr) { int len = arr.length; // 选择次数 for (int i = 0; i < len - 1; i++) { int min = i; // 每次选择过程 for (int j = i + 1; j < len; j++) { if (arr[j] < arr[min]) { min = j; } } if (min != i) { MyUtils.swap(arr, i, min); } } return arr; }
插入排序:每次把未排序的第一个数,插入到已排序数组的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
public static int[] sort(int[] arr) { int len = arr.length; // 插入次数,left为未有序的左边 for (int left = 1; left < len; left++) { int temp = arr[left]; int right = left - 1; // right为有序部分的右边 while (right >= 0 && temp < arr[right]) { arr[right + 1] = arr[right]; right--; } // 判断是否需要插入 if (right != left - 1) { arr[right + 1] = temp; } } return arr; }
归并排序:将数组分成很多小份,然后依次合并
public static int[] sort(int[] arr) { sort(arr, 0, arr.length - 1); return arr; } private static void sort(int[] arr, int left, int right) { if (left == right) { return; } // 等同于(right + left)/2 int mid = left + ((right - left) >> 1); sort(arr, left, mid); sort(arr, mid + 1, right); // 已经分成了许多小份,开始合并 merge(arr, left, mid, right); } private static void merge(int[] arr, int left, int mid, int right) { int[] help = new int[right - left + 1]; int i = 0; int p1 = left; int p2 = mid + 1; // 左边右边通过辅助数组合并 while (p1 <= mid && p2 <= right) { help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } // 左边没空加到后面 while (p1 <= mid) { help[i++] = arr[p1++]; } // 右边没空加到后面 while (p2 <= right) { help[i++] = arr[p2++]; } for (int j = 0; j < help.length; j++) { arr[left + j] = help[j]; } }
荷兰国旗问题:给定一个整数数组,给定一个值K,这个值在原数组中一定存在,要求把数组中小于K的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间,最终返回一个整数数组,其中只有两个值,分别是等于K的数组部分的左右两个下标值
public static int[] sort(int[] arr) { partiton(arr, 0, arr.length - 1); return arr; } public static int[] partiton(int[] arr, int left, int right) { int less = left - 1; int more = right + 1; int pNum = arr[right]; while (left < more) { if (arr[left] < pNum) { MyUtils.swap(arr, ++less, left++); } else if (arr[left] > pNum) { MyUtils.swap(arr, --more, left); } else { left++; } } return new int[]{less, more}; }
快速排序:重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作,递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
// 基于荷兰国旗问题的快排 public static int[] sort(int[] arr) { sort(arr, 0, arr.length - 1); return arr; } public static void sort(int[] arr, int left, int right) { if (left < right) { int[] pIndexs = DutchFlag.partiton(arr, left, right); sort(arr, left, pIndexs[0]); sort(arr, pIndexs[1], right); } }
堆排序:先建立大根堆,然后不停做heapify,也就是把未有序的最后一位和堆首互换,然后调整堆结构
public static int[] sort(int[] arr) { int len = arr.length; buildBigHeap(arr, len); while (len > 0) { MyUtils.swap(arr, 0, --len); heapify(arr, 0, len); } return arr; } // 建立大根堆 public static void buildBigHeap(int[] arr, int len) { for (int index = 0; index < arr.length; index++) { while (arr[index] > arr[(index - 1) / 2]) { MyUtils.swap(arr, index, (index - 1) / 2); index = (index - 1) / 2; } } } // 调整堆 private static void heapify(int[] arr, int currRoot, int len) { int left = currRoot * 2 + 1; int right = currRoot * 2 + 2; while (left < len) { int largest = right < len && arr[left] < arr[right] ? right : left; largest = arr[largest] > arr[currRoot] ? largest : currRoot; if (largest == currRoot) { break; } MyUtils.swap(arr, currRoot, largest); currRoot = largest; left = currRoot * 2 + 1; right = currRoot * 2 + 2; } }
二叉树
前序 中序 后续 层级遍历
public static void pre(TreeNode root) { if (root != null) { Stack<TreeNode> stack = new Stack<>(); stack.push(root); // 先进右再进左 while (!stack.isEmpty()) { root = stack.pop(); System.out.print(root.val + " -> "); if (root.right != null) { stack.push(root.right); } if (root.left != null) { stack.push(root.left); } } } System.out.println(); } public static void preReur(TreeNode root) { if (root == null) { return; } System.out.print(root.val + " -> "); preReur(root.left); preReur(root.right); }
public static void mid(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); // 左走到头了开始弹,然后去右 while (root != null || !stack.isEmpty()) { if (root != null) { stack.push(root); root = root.left; } else { root = stack.pop(); System.out.print(root.val + " -> "); root = root.right; } } System.out.println(); } public static void midReur(TreeNode root) { if (root == null) { return; } midReur(root.left); System.out.print(root.val + " -> "); midReur(root.right); }
public static void post(TreeNode root) { // 把线序遍历反过来,得到前右左,然后再反过来变成左右前 if (root != null) { Stack<TreeNode> stackStack = new Stack<>(); Stack<TreeNode> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { root = stack.pop(); stackStack.push(root); if (root.left != null) { stack.push(root.left); } if (root.right != null) { stack.push(root.right); } } while (!stackStack.isEmpty()) { System.out.print(stackStack.pop().val + " -> "); } } System.out.println(); } public static void postReur(TreeNode root) { if (root == null) { return; } postReur(root.left); postReur(root.right); System.out.print(root.val + " -> "); }
public static void level(TreeNode root) { if (root == null) { return; } LinkedList<TreeNode> queue = new LinkedList<>(); queue.add(root); TreeNode curr = null; while (!queue.isEmpty()) { curr = queue.pop(); System.out.print(curr.val + " -> "); if (curr.left != null) { queue.add(curr.left); } if (curr.right != null) { queue.add(curr.right); } } }
算法验证对数器
- 准备样本随机生成器
- 准备一个绝对正确但是复杂度不好的算法
- 将待验证算法和绝对正确算法压测,比较
主定理与递归时间复杂度的计算
- 主定理:如果有一个问题规模为 n,递推的子问题数量为 a,每个子问题的规模为n/b(假设每个子问题的规模基本一样),递推以外进行的计算工作为 f(n)(比如归并排序,需要合并序列,则 f(n)就是合并序列需要的运算量),那么对于这个问题有如下递推关系式:
- 然后就可以套公式估算递归的时间复杂度
B树和B+树定义与区别
- M阶B树
- 定义
- 任意非叶子结点最多只有M个儿子,且M>2
- 根结点的儿子数为[2, M]
- 除根结点以外的非叶子结点的儿子数为[M/2, M],向上取整
- 非叶子结点的关键字个数=儿子数-1
- 所有叶子结点位于同一层
- k个关键字把节点拆成k+1段,分别指向k+1个儿子,同时满足查找树的大小关系
- 特征
- 关键字集合分布在整颗树中
- 任何一个关键字出现且只出现在一个结点中
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
- 定义
- M阶B+树
- 定义
- 有n棵子树的非叶子结点中含有n个关键字(b树是n-1个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b树是每个关键字都保存数据)
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
- 所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字
- 通常在b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点
- 同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素
- 特征
- b+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”
- b+树查询必须查找到叶子节点,b树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢)
- 对于范围查找来说,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历
- 定义
并查集
- 用于解决
- 两个元素是否在同一个集合(优化,查的过程中把路过的节点直接连头节点)
- 合并两个元素所在的集合
- 实现
- 数组
- 双HashMap
红黑树
- 特点
- 每个节点非红即黑
- 根节点总是黑色的
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
- 每个叶子节点都是黑色的空节点
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
跳跃表
特点
- 最底层包含所有节点的一个有序的链表
- 每一层都是一个有序的链表
- 每个节点都有两个指针,一个指向右侧节点(没有则为空),一个指向下层节点(没有则为空)
- 必备一个头节点指向最高层的第一个节点,通过它可以遍历整张表
前缀树/字典树(Trie)
- 用于解决
- 常用于快速检索
- 大量字符串的排序和统计
- 基本性质
- 根节点不包含字符,除根节点外每个节点只包含一个字符
- 从根节点到某个节点,路径上所有的字符连接起来,就是这个节点所对应的字符串
- 每个节点的子节点所包含的字符都不同
如何从暴力递归改动态规范
- 首先写好一个暴力递归
- 分析这个递归是否有重复计算
- 分析这个递归的当前状态和之前递归计算的顺序是不是无关
- 如果都满足就可以改成动态规划
- 改写成DP
- 找出递归中变化的参数
- 确定递归的开始位置
- 确定一些边界或者特殊情况
- 抽象出一次递归的步骤,分析步骤和已经固定的边界的关系
- 找出规律后coding
布隆过滤(Bloom Filter)
解决问题:判断一个元素是否在一个集合中,优势是只需要占用很小的内存空间以及有着高效的查询效率
原理:保存了很长的二进制向量,同时结合 Hash 函数实现
- 首先需要k个hash函数,每个函数可以把key散列成为1个整数
- 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
- 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
- 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
特点
- 只要返回数据不存在,则肯定不存在
- 返回数据存在,只能是大概率存在
- 不能清除其中的数据
计算误差
先根据样本大小n,可以接受的误差p,计算需要申请多大内存m
再由m,n得到hash function的个数k
再计算实际的误差p