算法训练营
时间复杂度、空间复杂度
常数阶O(1)
对数阶O(logN)
线性阶O(n)
线性对数阶O(nlogN)
平方阶O(n²)
立方阶O(n³)
K次方阶O(n^k)
指数阶(2^n)
一维数据结构:线性表(array、stack、listNode、queue)、散列表(set、dictionary)
二维数据结构:堆、树、图
树是特殊的图:每个结点下有两个子结点
链表是特殊的树:每个结点下只有1个子节点
堆就是树,堆分为最大堆或者最小堆(根节点最大或者最小)
1、堆中某个节点的值总是不大于或者不小于其父节点的值
2、堆总是一颗完全二叉树
二叉树的 前序、中序、后续
前序:根-左-右
中序:左-根-右
后续:左-右-根
二叉搜素树、二叉查找树、有序二叉树、排序二叉树,是指一颗空树或者具有下列性质的二叉树
1、左子树上所有结点的值均小于它的根结点的值
2、右子树上所有结点的值均大于它的根结点的值
3、以此类推:左、右子树也分别为二叉查找树
中序遍历:升序遍历
广度优先
深度优先
递归代码模板:
func recursion(level,param1,param2,...) {
// recursion terminator 递归终结条件
if level > MAX_LEVEL
process_result
return
// pricess logic in current level 处理当前层级的业务逻辑
process(level,data,...)
// drill down 进入到下一层去
self.recursion(level + 1,param1,param2,...)
// reverse the current level status if needed 如果需要清理当前层
}
递归的三个思维要点
1、抵制人肉递归
2、找最近重复性
3、数学归纳法思维
分治和回溯
分治和回溯是特殊的递归
二分查找的前提
1、目标函数单调性(递增或者递减)
2、存在上下界(bounded)
3、能够通过索引访问(index accessible)
动态规划
和递归或者分治 没有根本上的区别(关键看有无最优的子结构)
共性:找到重复子问题
差异性:最优子结构、中途可以淘汰次优解
关键点
1、最优子结构 opt[n] = best_of(opt[n-1],opt[n-2],...)
2、存储中间状态:opt[i]
3、递推公式(美名其曰:状态转移方程或者DP方程)
Fib: opt[n] = opt[n-1] + opt[n-2]
二维路径: opt[i,j] = opt[i+1,j] + opt[i,j+1];(且判断a[i,j]是否空地)
动态规划小结
1、打破自己的思维惯性,形成机械思维
2、理解复杂逻辑的关键
3、职业进阶的要点要领,信任下属并merge反馈
字典树、trie树
并查集
适用场景:组团和配对问题
group or not ?
平衡二叉树
AVL树、红黑树、B树
AVL树
平衡因子:深度不超过绝对值1 【-1,0,1】
旋转操作:
右右子树 -> 左旋
左左子树 -> 右旋
左右子树 -> 左右旋
右左子树 -> 右左
不足:结点需要存储额外信息,操作太频繁
红黑树:近似平衡二叉树
它能够确保任何一个结点的左右子树的==高度差小于两倍==。
- 每个结点要么是红色,要么是黑色
- 根结点是黑色
- 每个叶结点(NIL结点,空结点)是黑色的
- 不能有相邻接的两个红色结点
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
位运算
运算符:
- ~取反操作
- &与运算:同为1的时候为1
- |或运算:有1的时候为1
- ^异或运算:相同为0不同为1
- <<左移运算
- ‘>>’右移运算
tips
- 正数的反码和补码都与原码相同
- 负数取正数的二进制数,然后最高位补1
- 负数的反码为对该数的原码除符号位外各位取反
- 负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1
异或运算的特征
- 任何数和 0 做异或运算,结果仍然是原来的数,即 a ^ 0 = a
- 任何数和其自身做异或运算,结果是 0,即 a ^ a=0
- 异或运算满足交换律和结合律,即 a ^ b ^ a = a ^ a ^ b = b
常用整理:
- 判断奇偶:x % 2 == 1 -> (x & 1) == 1,x % 2 == 0 -> (x & 1) == 0
- x >> 1 -> x/2,即 x = x/2 -> x = x >> 1,mid = (left + right)/2 -> (left + right) >> 1
- x = x & (x - 1) 清零最低位的1
- x & -x -> 得到最低位的1
- x & ~x = 0 ~为取反
布隆过滤器
一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中
- 优点:空间效率和查询时间都远远超过一般的算法
- 缺点:有一定的误识别率和删除困难。
LRUCache
- 两个要素:大小、替换策略
- HashTable + Double LinkedList
- O(1)查询、修改、更新
- LFU - least frequently used 统计的频次
- LRU - least recently used 最少最近使用
排序算法
- 十大经典排序算法
比较类排序
- 交换排序:冒泡排序、快速排序
- 插入排序:简单插入排序、希尔排序
- 选择排序:简单选择排序、堆排序
- 归并排序:二路归并排序、多路归并排序
非比较类排序
- 计数排序
- 桶排序
- 基数排序
高级排序
- 快速排序
1、取数组标杆pivot,将小元素放pivot左边的子数组,大元素放右边的子数组;
2、依次对两个子数组进行快排,以达到整个序列有序。
public static void quickSort(int[] array, int begin, int end) {
if (end <= begin) return;
int pivot = partition(array, begin, end);
quickSort(array, begin, pivot - 1);
quickSort(array, pivot + 1, end);
}
// 找出标杆的位置并将其左右排好序
static int partition(int[] a, int begin, int end) {
// pivot: 标杆位置,counter: 小于pivot的元素的个数
int pivot = end, counter = begin;
for (int i = begin; i < end; i++) {
if (a[i] < a[pivot]) {
int temp = a[counter]; a[counter] = a[i]; a[i] = temp;
counter++;
}
}
int temp = a[pivot]; a[pivot] = a[counter]; a[counter] = temp;
return counter;
}
- 归并排序
1、把长度为n的输入序列分成两个长度为n/2的子序列;
2、对着两个子序列分别采用归并排序;
3、将两个排序好的子序列合并成一个最终的排序序列;
public static void mergeSort(int[] array, int left, int right) {
if (right <= left) return;
//(left + right) / 2 位运算更加快速
int mid = (left + right) >> 1;
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
merge(array, left, mid, right);
}
// 将排好序的两个数组合并
public static void merge(int[] arr, int left, int mid, int right) {
// 中间数组,额外的空间复杂度
int [] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) temp[k++] = arr[i++];
while (j <= right) temp[k++] = arr[j++];
// 将排序好的数组放回arr中
for (int p = 0 ; p < temp.lent; p++) {
arr[left + p] = temp[p];
}
}
- 堆排序
1、 数组元素一次建立小顶堆
2、依次取堆顶元素,并删除
void heap_sort(int a[], int len) {
priority_queue, greater> q;
for(int i = 0; i < len; i++){
q.push(a[i]);
}
for(int i = 0; i < len; i++) {
a[i] = q.pop();
}
}
归并 和 快排 具有相似性,但步骤顺序相反
归并: 先排序左右子数组,然后合并两个有序子数组
快排: 先调配处左右两个子数组,然后对左右子数组进行排序