算法
时间复杂度
T(n) = O(f(n)) 大O计算法 代码执行次数最多原则进行计算
1.O(1) 常量级
可数的量时即为O(1) 如循环次数为100次也为O(1)
2.O(n) 线性级
时间复杂度根据n增长而增长
3.O(log2n)、O(nlog2n) 对数级
执行x次大于n时,2x=n -> x=log2 n
wihle(i<n){
i = i*2
}
空间复杂度
1.O(1) 常量级
可数的量时即为O(1) 如int i = 0;
2.O(n) 线性级
空间复杂度根据n增长而增长 如 int[] arr = new arr[n]
递归算法
方法自己调用自己,每次参数不一。
简单递归实例
public int f(int n){
if(n==1) return 1;
return f(n-1)+1;
}
运用条件:1.具有递推的规律 2.具有出口的条件
问题 | 解决 | |
---|---|---|
堆栈溢出 | 加入终止条件,不要让递归次数太多 | |
重复计算 | 通过散列表存储计算过的值,当重复计算时从散列表中取 |
所有循环都可以写成递归,递归不一定可以写成循环
应用:
-
斐波那契数列:
每一项等于前两项之和,当n>=3时f(n) = f(n-1)+f(n-2)
-
阶乘:
值求解较小时,使用int即可 若值比较大时,使用BigInteger 当BigInteger依旧满足不了时 采用数组的方式分布拆除计算
算法思想
贪心算法 greedy algorithm
又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。在不同情况,选择最优的解,可能会导致辛普森悖论(Simpson’s Paradox),不一定出现最优的解。
贪心算法在数据科学领域被广泛应用,特别是金融工程。其中一个贪心算法例子就是Ensemble method。
实现过程
- 创建数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的解局部最优解合成原来解问题的一个解。
分治法
在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范型。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)
另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。
分治法这个名称有时亦会用于将问题简化为只有一个细问题的算法,例如用于在已排序的列中查找其中一项的折半搜索算法(或是在数值分析中类似的勘根算法)。这些算法比一般的分治算法更能有效地运行。其中,假如算法使用尾部递归的话,便能转换成简单的循环。但在这广义之下,所有使用递归或循环的算法均被视作“分治算法”。因此,有些作者考虑“分治法”这个名称应只用于每个有最少两个子问题的算法。而只有一个子问题的曾被建议使用减治法这个名称。
分治算法通常以数学归纳法来验证。而它的计算成本则多数以解递归关系式来判定。
实现过程:
- 分解:将原问题分解为若干个规模较小,相对独立,与原问题形式相同的子问题。
- 解决:若子问题规模较小且易于解决时,则直接解。否则,递归地解决各子问题。
- 合并:将各子问题的解合并为原问题的解。
回溯法 backtracking
是暴力搜索法中的一种。对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束补偿问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)。
在经典的教科书中,八皇后问题展示了回溯法的用例。(八皇后问题是在标准国际象棋棋盘中寻找八个皇后的所有分布,使得没有一个皇后能攻击到另外一个。)
回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案
- 在尝试了所有可能的分步方法后宣告该问题没有答案
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
实现过程:
-
尝试:每次遇到分支就选择一项未经历的路径进行尝试,直到达到正确或失败的结果
-
回溯:当遇到了正确或失败的结果,回退到上一个节点进行其他路径尝试
动态规划 DP Dynamic programming
是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
适用情况
- 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
- 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。
排序算法
判定条件
1. 时间复杂度 : 同阶需要考虑 系数、常数、低阶
2. 空间复杂度 : out-place非原地排序,占用额外内存、in-place原地排序 复杂度O(1)只占用常数内存
3. 算法稳定性 : 多次操作结果不会改变,即使值相同
排序场景 | 排序效率 |
---|---|
Random(随机数列) | 希尔>快排>归并 |
Few unique(少量数列) | 快排>希尔>归并 |
Reversed(反向数列) | 快排>希尔>归并 |
Almost sorted(几乎接近排序数列) | 插入排序>基数排序>快排>希尔>归并 |
注:Arrays.sort() 在小于47时使用插入排序,47~286时使用快速排序,大于286时 判断是否具备归并结构(分组小于67组)使用归并,否则使用快排
比较排序算法 Comparison Sorting Algorithms
插入排序 insertion sort
/**
* 插入排序 - 直接插入
* 遍历数组,将当前的值往前比较,将前面所有比当前值大的值往后移动一位,将该值插入空出的位。
* 即相当于每次遍历一个都会将此数插入值正确的位置,
* 遍历至n处,即n前数组的排序是正确的
*
* @param arr 待排序数组
*/
public static void insertSort(int[] arr) {
int len = arr.length;
if (len <= 1){
return;
}
for (int i = 1; i < len; i++) {
//为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
int j = i - 1;
for (; j >= 0; j--)
if (arr[j] < arr[i])
break;
//如找到了一个合适的位置
if (j != i - 1) {
//将比a[i]大的数据向后移
int temp = arr[i];
int k = i - 1;
for (; k > j; k--)
arr[k + 1] = arr[k];
//将a[i]放到正确位置上
arr[k + 1] = temp;
}
}
}
选择排序 selection sort
/**
* 选择排序
* 遍历数组,每次找到最小或最大的数,放在起始位
* @param arr
*/
public static void selectSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
//定义最小值的索引
int minIndex = i;
for (int j = i+1; j < arr.length; j++) {
//当找到比最小值小的值的,更换最小值索引
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
//最小值不是此循环起始,则进行换位
if(minIndex != i){
swap(arr,i,minIndex);
}
}
}
冒泡排序 bubble sort
/**
* 冒泡排序
* 与冒泡赛机制相同,
* 排位靠低的队伍进行比赛,A VS B A > B 时 A B交换
* 从而依次决定第一名、第二名。。。。。。第n名
*
* 根据数值特性,最大的我们放在最后即
* n名,n-1名。。。。。2名,1名
*
* @param arr
*/
public static void bubbleSort(int[] arr){
//若后续无换位提前结束循环
boolean flag = false;
int arrLen = arr.length;
for (int i = arrLen-1; i > 0; i--) {//确定名次
for (int j = 0; j < i ; j++) {//低升高
if(arr[j] > arr[j+1]){
swap(arr,j,j+1);
flag = true;
}
}
if(!flag){
break;
}
}
}
快速排序 Quick sort
/**
* 快速排序
* 定义一个基准数,数组边界。
* x = arr[i] i -> j
* 从j往前找 arr[j1] < x 的数,找到了就与arr[i] 换位 并从i+1开始往后找 arr[i1] > x 的数,找到了就与arr[j1]交换
* 最后 将arr[i1] 赋值为 x 即 i->i1 为比x小的数, i1 -> j 为比x大的数。再递归找 i->i1-1 i1+1->j 即可快速排序
*
* [i,..............i1.............j1....,j]
* x <----j arr[j1] < x 与替换i
*arr[j1] i+1------>i1 arr[i1] > x 与替换j1
*arr[j1] i1 <----j1-1 arr[i1] j1-1到i1没有找到比x小的数,结束当次排序
*arr[j1] x arr[i1]...,j] 将i1设置为基准数,即arr[i1] = x,i->i1-1 为比x小的数,i1+1->j 为比x大的数
*
* @param arr 待排序数组
* @param start 左边界
* @param end 右边界
*/
public static void quickSort(int[] arr,int start,int end){
if(start >= end){
return;
}
//定义边界与基准数
int i =start,j=end;
int x = arr[i];
while (i < j){
//从j往前找比x小的值
while (i < j && arr[j] > x){
j--;
}
//找到了且在边界里就替换i值
if (i < j){
arr[i++] = arr[j];
}
//从i往后找比x大的值
while (i < j && arr[i] < x){
i++;
}
//找到了且在边界内就替换j值
if (i < j){
arr[j--] = arr[i];
}
}
//将基准值替换i,以i分隔进行排序
arr[i] = x;
quickSort(arr,start,i-1);
quickSort(arr,i+1,end);
}
归并排序 Merge sort
/**
* 归并排序
* 1.分解 2.求解 3.合并
* @param arr
*/
public static void mergeSort(int[] arr){
//定义一个临时归并空间,防止递归中新开辟空间
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length - 1,temp);
}
/**
* 归并排序 分解
* 将排序数组每次平分 至 1个1个单独求解数,
* 在进行两两排序合并
*
* @param arr 数组
* @param left 左边界
* @param right 右边界
* @param temp 排序所需的临时空间
*/
private static void mergeSort(int[] arr,int left,int right,int[] temp){
if(left < right){
int mid = left + (right-left)/2;
//分解
mergeSort(arr,left,mid,temp);
mergeSort(arr,mid+1,right,temp);
//排序
mergeSort(arr,left,mid,right,temp);
}
}
/**
* 归并排序 排序、合并
* [2,4,5,657,5,2,34,54,0,12]
* len = 10 left 0 right 9
*分解 left=0 right=4 || left=5 right=9
* 0-2 || 3-4 5-7 || 8-9
* 0-1||2-2 3-3||4-4 5-6||7-7 8-8||9-9
* 0-0||1-1 5-5||6-6
*
*合并
* 第一层合并
* 0-0[2]<->1-1[4] = 0-1 temp [2,4] -> arr [{2,4},5,657,5,2,34,54,0,12]
* 5-5[2]<->6-6[34] = 5-6 temp [2,34] -> arr [2,4,5,657,5,{2,34},54,0,12]
* 第二层合并
* 0-1[2,4]<->2-2[5] = 0-2 temp [2,4,5] -> arr [{2,4,5},657,5,2,34,54,0,12]
* 3-3[657]<->4-4[5] = 3-4 temp [5,657] -> arr [2,4,5,{5,657},2,34,54,0,12]
* 5-6[2,34]<->7-7[54] = 5-7 temp [2,34,54] -> arr [2,4,5,5,657,{2,34,54},0,12]
* 8-8[0]<->9-9[12] = 8-9 temp[0,12] -> arr [2,4,5,5,657,2,34,54,{0,12}]
* 第三层合并
* 0-2[2,4,5]<->3-4[5,657] = 0-4 temp [2,4,5,5,657] -> arr [{2,4,5,5,657},2,34,54,0,12]
* 5-7[2,34,54]<->8-9[0,12] = 5-9 temp [0,2,12,34,54] -> arr [2,4,5,5,657,{0,2,12,34,54}]
* 第四层合并
* 0-4[2,4,5,5,657]<->5-9[0,2,12,34,54] = 0-9 temp[0,2,2,4,5,5,12,34,54,657] -> arr [{0,2,2,4,5,5,12,34,54,657}]
*
* 排序后 arr = [0,2,2,4,5,5,12,34,54,657]
*
* @param arr 排序数组
* @param start 开始索引
* @param mid 分割索引
* @param end 结束索引
* @param tmp 合并临时空间
*/
private static void mergeSort(int[] arr, int start, int mid, int end,int[] tmp) {
// 第1个有序区(arr start->mid)的索引
int i = start;
// 第2个有序区(arr mid+1->end)的索引
int j = mid + 1;
// 临时区域的索引
int k = 0;
//根据小到大顺序合并两个有序区至临时区域
while(i <= mid || j <= end) {
//当j越界或1区值小于2区值时,取1区值,否则取2区值
if (j > end || (i <= mid && arr[i] <= arr[j])) {
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
// 将排序后的元素,全部都整合到数组a中。
for (i = 0; i < k; i++) {
arr[start + i] = tmp[i];
}
}
希尔排序 Shell sort
/**
* 希尔排序 缩小增量排序
* 分组插入排序 希尔排序在效率上较直接插入排序有较大的改进。
* 定义增量gap,进行分组排序 0 - gap - gap+gap.. 1 - gap+1 - 2gap+1.. < arr.length
* 每次增量排完序后,继续递减增量gap进行分组插入排序,直到增量1即为最后次排序
*
* @param arr 待排序数组
*/
public static void shellSort(int[] arr){
int gap = arr.length;
//当增量为1时,即只有长度为数组长度的一组进行排序
while (gap >= 1){
//增量递减,单次排序量增加
gap/=2;
//定义分组增量 0~gap为增量 i为第一组元素
for (int i = 0; i < gap; i++) {
// length / gap 为组数,每一组进行插入排序,每组最少2个数 j为后序组元素
for (int j = i+gap; j < arr.length; j+=gap) {
//k 为 j前一组元素
int k = j - gap;
// 如果 k > j 则继续往前查找 将j 放入正确的位置 此处 相当于 k = k ; k+gap = j
// 往前搜索 k = k - gap; k+gap = k
//......
while (k >= 0 && arr[k] > arr[k+gap]) {
swap(arr,k,k+gap);
k -= gap;
}
}
}
}
}
桶排序 Bucket Sort
/**
* 桶排序
* [23,221,431,412,434,545,65,456,234,567] arr len=10,该数组均为最大不超过1000的数,于是我们可以定义1000为最大边界
*
* 定义桶 长度为10,每个桶范围为 100 即存储范围如下
*[[0-99],[100-199],[200-299]....[900-999]]
* 将数组中数据依次放入桶中,如同一桶中已有值按有序列表排列
* [[23,65],[],[221,234],[],[412,431,434,456],[545,567],[],[],[],[],[],[]]
*
* 再将桶中数替换为原数组
* [23,65,221,234,412,431,434,456,545,567]
*
* @param arr 排序数组
* @param max 最大边界,且arr中没有大于等于max的数
* @param bucket_len 桶大小
*/
public static void bucketSort(int[] arr,int max,int bucket_len){
int len = arr.length;
if(len<=1){
return;
}
//获取最大数
// int max = Integer.MIN_VALUE;
// for (int i = 0; i < arr.length; i++) {
// if(max < arr[i]){
// max = arr[i];
// }
// }
// max = max+1;
//定义桶大小,也可根据数组大小变化
// int bucket_len = 6000;
BucketNode[] buckets = new BucketNode[bucket_len];
//定义桶范围
int bucket_range = max / bucket_len;
//将目标排序数组装入对应桶中
for (int i = 0; i < len; i++) {
//确保最大数能够装入桶中,并根据桶长度进行划分桶的范围,所以桶排序适合数量差异接近能够均分
int bucketIndex = arr[i] / bucket_range;
BucketNode bucketNode = new BucketNode(bucketIndex, arr[i]);
//当桶为空,直接放入当前数
if(buckets[bucketIndex] == null){
buckets[bucketIndex] = bucketNode;
}else{
//取出桶中 链头
BucketNode head = buckets[bucketIndex];
//比较顺序,返回新的链头
BucketNode newHead = head.setNext(bucketNode);
if(head != newHead){
buckets[bucketIndex] = newHead;
}
}
}
//从桶中取出数据 放回数组
int a_index = 0;
for (int i = 0; i < buckets.length; i++) {
if(buckets[i] != null){
BucketNode bucket = buckets[i];
while (bucket != null){
arr[a_index++] = bucket.value;
bucket = bucket.next;
}
}
}
}
static class BucketNode{
int bucketIndex;
int value;
BucketNode next;
public BucketNode(int bucketIndex,int value){
this.bucketIndex = bucketIndex;
this.value = value;
}
/**
* 设置有序链表(从小到大)
* @param n 值
* @return 链表头
*/
public BucketNode setNext(BucketNode n){
if(n == null){
return this;
}
//目标节点比链头小,目前节点作为顶点返回
if(this.value > n.value){
n.next = this;
return n;
}
//目标节点大于等于链头
else{
if(this.next == null){
this.next = n;
}else{
BucketNode node = this;
while (node.next != null){
if(node.next.value > n.value){
n.next = node.next;
node.next = n;
break;
}else{
node = node.next;
if(node.next == null){
node.next = n;
break;
}
}
}
}
return this;
}
}
}
计数排序 Counting Sort
基数排序 Radix Sort
堆排序 Heap Sort
/**
* 堆排序(从小到大)
*
* 参数说明:
* a -- 待排序的数组
* n -- 数组的长度
*/
public static void heapSort(int[] a) {
int n = a.length;
// 从(n/2-1) --> 0逐次遍历, 设置所有非叶子节点(根)为其左右子树的最大值。
for (int i = n / 2 - 1; i >= 0; i--) {
maxHeapDown(a, i, n-1);
}
// 从最后一个元素开始对序列进行调整,不断的缩小调整的范围直到第一个元素
for (int i = n - 1; i > 0; i--) {
// 交换a[0]和a[i]。因为当前已经是最大二叉堆,a[0]一定是最大值,将a[i]与a[0]交换,
swap(a,0,i);
// 再对 0 - i-1 进行最大二叉堆调整,使得a[0...i-1]仍然是一个最大堆。再次保证a[0]是a[0...i-1]中的最大值。
maxHeapDown(a, 0, i-1);
}
}
/**
* (最大)堆的向下调整算法
*
* 注: 数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
* 其中,N为数组下标索引值,如数组中第1个数对应的N为0。
*
* 参数说明:
* a -- 待排序的数组
* start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
* end -- 截至范围(一般为数组中最后一个元素的索引)
*/
public static void maxHeapDown(int[] a, int start, int end) {
// 当前(current)节点的位置
int c = start;
// 左(left)孩子的位置
int l = 2*c + 1;
while (l <= end){
int r = l + 1;
if(r <= end && a[l] < a[r]){
l = r;
}
if(a[c] >= a[l]){
break;
}else{
swap(a,c,l);
}
c = l;
l = 2*c + 1;
}
}
字符串匹配算法
暴风算法(暴力算法) BF Brute Force
使用子串对主串逐一匹配
/**
* 暴风匹配法 Brute Force 暴力算法
*
* 逐一匹配是否相等
* 将主串t分割为子串p长度的多个子串,依次与子串p匹配
* @param t 主串
* @param p 子串
* @return 返回子串在主串第一次出现的索引 -1 表示未能匹配
*/
public static int indexOfBF(String t,String p){
if(isEmpty(t) && isEmpty(p) && t.length() < p.length()){
return -1;
}
int a = 0;
int b = 0;
while (a < t.length() && b < p.length()){
if(t.charAt(a) == p.charAt(b)){
a++;
b++;
}
else{
a = a - b + 1;
b = 0;
}
}
if(b == p.length()){
return a - p.length();
}
return -1;
}
哈希表算法 RK (拉宾,卡普) Rabin–Karp algorithm
该算法先使用旋转哈希以快速筛出无法与给定串匹配的文本位置,此后对剩余位置能否成功匹配进行检验。此算法可推广到用于在文本搜寻单个模式串的所有匹配或在文本中搜寻多个模式串的匹配。
* 哈希匹配法 RK算法
* 是对BF算法的一个改进
* 思想 如果两个字符串hash后的值不相同,则它们肯定不相同;如果它们hash后的值相同,它们不一定相同。
*
* 实践 将主串t分割为多个与子串p相同的子串,依次计算这些子串的哈希值,若哈希值与匹配子串p相等再校验每个字符是否相等;
* @param t 主串
* @param p 子串
* @return 返回子串在主串第一次出现的索引 -1 表示未能匹配
*/
public static int indexOfRK(String t,String p){
if(isEmpty(t) && isEmpty(p) && t.length() < p.length()){
return -1;
}
int p_len = p.length();
char[] t_chars = t.toCharArray();
char[] p_chars = p.toCharArray();
int p_hash = hash(p_chars, 26, 31, 0, p_len);
for (int i = 0; i < t.length() - p_len + 1; i++) {
int t_hash = hash(t_chars, 26, 31, i, p_len);
if(t_hash == p_hash){
for (int j = i; j < i+p_len; j++) {
if(t_chars[j] == p_chars[j-i]){
if(j == i+p_len-1){
return i;
}
}else{
break;
}
}
}
}
return -1;
}
/**
* 计算哈希值
* @param chars 字符集
* @param R 进制 26 16 8 等,字符串一般使用26进制
* @param K 位
* @param begin 开始索引
* @param len 长度
* @return
*/
private static int hash(char[] chars,int R,int K,int begin,int len){
int hash = 0;
for (int i = begin; i < begin+len; i++) {
hash = hash*K + chars[i];
}
return hash%R ^ (hash >>> 16);
}