常用算法总结

来源:互联网 发布:淘宝产品短连接 编辑:程序博客网 时间:2024/06/18 08:43
别人总结的算法面经:http://blog.csdn.net/yangcs2009/article/details/38140293
一,算法基础
O(..)表示复杂度小于等于数量级 ; o(...)表示复杂度小于数量级;
复杂度数量级:n!>指数>幂>对数>常数;     复杂度大于等于指数的为NP问题。
 
二,分治策略
适合分治策略的问题:
该问题的规模缩小到一定的程度就可以容易地解决;
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
利用该问题分解出的子问题的解可以合并为该问题的解;
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

2.1,二分法:只适用于有序数组,链表不可用。复杂度O(logn)
public static int search(int[] arr, int key) {       int start = 0;       int end = arr.length - 1;       while (start <= end) {           int middle = (start + end) / 2;           if (key < arr[middle]) {               end = middle - 1;           } else if (key > arr[middle]) {               start = middle + 1;           } else {               return middle;           }       }       return -1;   }}

最坏情况下的比较次数:那个答案(log2n+ 1)下取整 或者(log2 (n + 1) )上取整。
即使找不到结果,最后的middle值就在key的左或者右。

在Rotated Array中求最值的题目需要注意,此类题重点在于找规律,通过mid与其他值得比较对范围进行筛选。
Find Minimum in Rotated Sorted Array 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2
常用算法总结 - Garfield - 张广辉的博客
 

2.2,分治策略时尽量将子问题等分,这样复杂度低。或者尽量将分开的比例保持不变。快排的最差情况就是等分点总是在数组两头。
2.3,分治策略的优化
常用算法总结 - Garfield - 张广辉的博客
优化a,减少子问题数;优化f(n),递归之前做相关预处理

典型应用:

2.4 快速排序
特性:unstable sort、In-place sort。
最坏运行时间:当输入数组已排序时,时间为O(n^2)
最佳运行时间:O(nlgn),将划分左右比例取常数时,复杂度最低。

常用算法总结 - Garfield - 张广辉的博客
 a[j]<=x 把比首元素小的元素全都排到前面。
常用算法总结 - Garfield - 张广辉的博客
 

以下这种写法可以使和首值相等的聚集到一起,返回的k为最后一个相等值的索引。而上面的写法做不到。
常用算法总结 - Garfield - 张广辉的博客
 


2.5 快排可以通过随机选取标准数进行优化。因为如果每次取得的首元素将数组分为大小非常不均匀的两数组,将导致复杂度增加。
常用算法总结 - Garfield - 张广辉的博客


 
2.6 归并排序
特点:stable sort、Out-place sort 
思想:运用分治法思想解决排序问题。
最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)
空间复杂度:O(n)
相对于快排,归并排序要额外的空间。
http://blog.csdn.net/middlekingt/article/details/8446552

2.7,左中右计算最值问题

三,动态规划
动态规划是用空间换时间的一种方法的抽象。其关键是发现子问题和记录其结果。然后利用这些结果减轻运算量。常用于最优化问题的求解。
设计要点:寻找子问题,确定子问题到当前问题的递推关系,迭代记录中间最优结果。
优化原则:要看看子问题大小是否决定了主问题,并不是所有组合优化问题都适用,比如求模最小值。
与蛮力算法的不同:递归也好,枚举路径也好,在这里都算是蛮力算法,因为到达一个中间点并没有记录中间结果,这样就对中间点多次的计算,导致时间复杂度升高。动态规划记录中间结果,只保留最优的中间值,增加了空间复杂度,降低了时间复杂度。

动态规划实例
1)最短路径问题,m代表每层节点个数,n代表层数。如果枚举路径,需要O(m*2的n次方);而每一步都记录中间最有结果,需要O(mn);

2)矩阵连乘问题:xy矩阵*yz矩阵,需要进行xyz次计算。

给定n个矩阵{A1,A2,…,An},其中AiAi+1是可乘的,i=1,2,…,n-1。考察这n个矩阵的连乘积A1A2…An。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,则可以依此次序反复调用2个矩阵相乘的标准算法(有改进的方法,这里不考虑)计算出矩阵连乘积。若A是一个p×q矩阵,B是一个q×r矩阵,则计算其乘积C=AB的标准算法中,需要进行pqr次数乘。

矩阵连乘积的计算次序不同,计算量也不同,举例如下:

先考察3个矩阵{A1,A2,A3}连乘,设这三个矩阵的维数分别为10×100100×55×50。若按((A1A2A3)方式需要的数乘次数为10×100×510×5×507500,若按(A1A2A3))方式需要的数乘次数为100×5×5010×100×5075000

下面使用动态规划法找出矩阵连乘积的最优计算次序。

1,  设矩阵连乘积AiAi+1…Aj简记为A[i:j],设最优计算次序在AkAk+1之间断开,则加括号方式为:

((AiAi+1…Ak)(Ak+1…Aj))

则依照这个次序,先计算A[i:k]A[K+1:j]然后再将计算结果相乘,计算量是:

A[i:k]的计算量加上A[K+1:j]的计算量再加上它们相乘的计算量。

问题的一个关键是:计算A[i:j]的最优次序所包含的两个子过程(计算A[i:k]A[K+1:j])也是最优次序。

2,  设计算A[i:j]所需的最少数乘次数为m[i][j]

i=j时为单一矩阵,则m[i][i]=0

i<j时,设最优计算次序在AkAk+1之间断开,则m[i][j]=m[i][k]+m[k+1][j]+pipk+1pj+1,其中p表示数组的维数,例如A0A56个数组,他们表示如下:

//p[0]:第一个矩阵的行数

    //p[1]:第一个矩阵的列数,第二个矩阵的行数

    //p[2]:第二个矩阵的列数,第三个矩阵的行数

k此时并未确定,需要从ij-1遍历以寻找一个最小的m[i][j]。我们把这个最小的k放在s[i][j]
以下是完整实现代码,以一个具体的例子实现,稍加修改即可通用。
http://yoyo08.iteye.com/blog/418547

3)0-1背包问题

用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

这个方程非常重要,据说基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以详细的查了一下这个方程的含义:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。


4) 两个字符串求最优问题(如求最长的公共子串长度)
此类题的难点在于找递推关系
首先对lcs(i,0)和lcs(0,i)进行初始化。

1. xi == yj, lcs(i,j) = lcs(i-1, j-1) + 1;

2. xi != yj , lcs(i, j) = max ( lcs( i-1, j) , lcs( i, j-1) );


四,贪心算法
1)概念:贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

2)贪心算法适用的问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

3)迪杰斯特拉算法(Dijkstra)
常用算法总结 - Garfield - 张广辉的博客
基本思想:将整个图的节点分成两个集合,第一个集合装已经找到最小路径的点S,第二个集合为未找到的店V。初始时S只有原点。建立如上表格,每次从V中选取距离最小的点到S,根据新加入的点更细表格。重复操作。其中更新过程可以表示为W2 = min{w1,w1+w2}
 个人认为该算法属于dp,因为虽然每次都选取最小值,但始终考虑的是从原点到终点一个全局的最小值。
其实此题也可以用标准dp来做,假如求1到3最短路径,可先求出1到2和1到4的最短路径,然后求出1到2到3和1到4到3的最短路径。 

五,回溯策略
1,深度优先搜索和广度优先搜索概念(找出所有的可行解):http://blog.csdn.net/andyelvis/article/details/1728378
DFS:从根节点开始访问他的子节点,以树的深度优先,不断访问子节点。当某一节点不再有子节点了,那么他将返回访问仍没有被访问的祖先节点的子节点。前序中序后序都属于DFS。前序遍历是父节点先于子节点;中序是父节点在两子节点之间,后序是父节点在子节点之后。


 DFS前序迭代写法(不常用)
public void DFS(TreeNode root){        if(root==null){            System.out.println("empty tree");            return;        }               ArrayDeque<TreeNode> stack=new ArrayDeque<TreeNode>();//定义栈        stack.push(root);               while(stack.isEmpty()==false){            TreeNode node=stack.pop();            System.out.print(node.value+"    ");                               //对节点的访问            if(node.right!=null){                stack.push(node.right);//先把右孩子压栈            }            if(node.left!=null){                stack.push(node.left);//左孩子压栈,先访问            }                   }        System.out.print("\n");    }

DFS递归写法
public void DFS(TreeNode root) {
        if (root==null)
            return;
        else {
            DFS(root.left);                     //至于是前序中序还是后序就把根节点的访问放在啥位置就好了。
            DFS(root.right));
       }
    }                               
BFS
(分层次搜索)
常用算法总结 - Garfield - 张广辉的博客
 

2,实例
1)8皇后问题:
常用算法总结 - Garfield - 张广辉的博客
8皇后之间需满足:1.不在同一行上;2.不在同一列上;3.不在同一斜线上; 4.不在同一反斜线上
深度优先搜索遍历的思路,我们可以逐行或者逐列来进行可行摆放方案的遍历,每一行(或列)遍历出一个符合条件的位置,接着就到下一行或列遍历下一个棋子的合适位置,接下来,我们只要判断当前位置是否还符合其他条件,如果符合,就遍历下一行(或列)所有位置,看看是否继续有符合条件的位置,以此类推,如果某一个行(或列)的所有位置都不合适,就返回上一行(或列)继续该行(或列)的其他位置遍历,当我们顺利遍历到最后一行(或列),且有符合条件的位置时,就是一个可行的8皇后摆放方案,累加一次八皇后可行方案的个数,然后继续遍历该行其他位置是否有合适的,如果没有,则返回上一行,遍历该行其他位置,依此下去。这样一个过程下来,我们就可以得出所有符合条件的8皇后摆放方案了。这是一个深度优先遍历的过程,同时也是经典的递归思路。

用一个三层的八叉树表示此算法就更为形象。书中所讲。

2)判断二叉树是否平衡(需要对DFS进行裁剪)
常用算法总结 - Garfield - 张广辉的博客
 
常用算法总结 - Garfield - 张广辉的博客
 
3)求组合If n = 4 and k = 2, a solution is:
[  [2,4],  [3,4],  [2,3],  [1,2],  [1,3],  [1,4],](像这种实际问题,都要先抽象成一棵多叉树来解决)
常用算法总结 - Garfield - 张广辉的博客

六,排序算法总结
1,排序算法的稳定性与速度比较
1)实验分析排序速度
数据规模    快速排序    归并排序    希尔排序    堆排序
1000万       0.75           1.22          1.77          3.57
5000万       3.78           6.29          9.48         26.54  
1亿             7.65          13.06        18.79        61.31

2)排序的稳定性
两个值相等的元素排序后先后位置不变,则为稳定排序。稳定排序与不稳定排序可以进行转化。比如把>的判断条件变为>=.。
稳定性的作用:一个班的学生已经按照学号大小排好序了,我现在要求按照年龄从小到大再排个序,如果年龄相同的,必须按照学号从小到大的顺序排列。那么问题来了,你选择的年龄排序方法如果是不稳定的,是不是排序完了后年龄相同的一组学生学号就乱了,你就得把这组年龄相同的学生再按照学号拍一遍。如果是稳定的排序算法,我就只需要按照年龄排一遍就好了。

2,插入排序:
特点:stable sort、In-place sort
最优复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
最差复杂度:当输入数组为倒序时,复杂度为O(n^2)
插入排序比较适合用于“少量元素的数组”。
其实插入排序的复杂度和逆序对的个数一样,当数组倒序时,逆序对的个数为n(n-1)/2,因此插入排序复杂度为O(n^2)。
常用算法总结 - Garfield - 张广辉的博客
改进的插入排序-Shell排序。因为插排在非常无序的情况下需要插入的次数多,复杂度很高。复杂度O(ns) 1<s<2
常用算法总结 - Garfield - 张广辉的博客
 

3,冒泡排序
特点:stable sort、In-place sort
思想:通过两两交换,像水中的泡泡一样,小的先冒出来,大的后冒出来。
最坏运行时间:O(n^2)
最佳运行时间:O(n^2)(当然,也可以进行改进使得最佳运行时间为O(n))
插入排序的速度直接是逆序对的个数,而冒泡排序中执行“交换“的次数是逆序对的个数,交换需要进行三步操作,因此插入排序的执行时间至少比冒泡排序快。
常用算法总结 - Garfield - 张广辉的博客
 
因为原始的冒泡排序在输入序列已经排好序的情况下还需要在对每个只进行一遍检测,所以最好的复杂度也是O(n2).改进一下,设置个标志位,当某一个巡回检测到巡回中的数列已经排序好时,就立即返回。因此最低复杂度为O(n)
常用算法总结 - Garfield - 张广辉的博客
 
4,选择排序
特性:In-place sort,unstable sort。
思想:每次找一个最小值。
最好情况时间:O(n^2)。
最坏情况时间:O(n^2)。
常用算法总结 - Garfield - 张广辉的博客常用算法总结 - Garfield - 张广辉的博客
 

5,堆排序
特性:unstable sort、In-place sort。
最优时间:O(nlgn)
最差时间:O(nlgn)
堆排序是选择排序的升级版,选择排序是通过对子数组的遍历找到子数组中当前的最小值,而堆排序每次把最大值放在根节点
 常用算法总结 - Garfield - 张广辉的博客常用算法总结 - Garfield - 张广辉的博客
 
6,基数排序
不需要比较的排序
特性:stable sort、Out-place sort。
最坏情况运行时间:O(n)
最好情况运行时间:O(n)
http://v.youku.com/v_show/id_XMzgzMDA0NDgw.html



七,OJ算法题注意事项
1,提交前一定要认真检查语法错误。
2,链表问题注意:
1)时刻注意链表是否会产生空指针问题,比如使用node.val或node.next时node当前是否为null;node2!=null&&node2.val==val
2)务必考虑一些特例情况,比如该链表为空,该链表只有一个节点等特殊情况,或者需要处理的节点在链表的两端。
3)链表中交换节点顺序的固定写法
newHead.next=swap(newHead.next,newHead.next.next);

 public ListNode swap(ListNode node1,ListNode node2) {
        node1.next=node2.next;
        node2.next=node1;
        
        return node2;
    }

3,位运算问题
一个二进制数对应两个十进制,分为有符号和无符号;一个十进制数只对应一个二进制数;比如一个8位二进制数10000000,当他为无符号数时为128,当他为有符号数时为-128;当他为有符号数时,要想知道他表示的是什么十进制,用取反加1来计算,二进制正负数转化,都是取反加一;0000000-----1111111----10000000为128;无符号数的范围为0---255;有符号数的范围为-128(10000000)---127(01111111)
在计算机程序中,不管是什么数,都转化为2进制数来进行计算;比如给定一个数(可能有符号可能无符号)来数他的二进制中有多少个1,那么直接用位运算&1来计算,不要牵扯十进制;当需要对十进制进行运算时,比如将十进制转化为二进制,如果是无符号的负数,需要先计算出他的正数,再取反加一;整数采用“除2取余,逆序排列”,小数采用"乘2取整,顺序排列"法。
不管是64位系统还是32位系统,int都是4个字节长度;其中java中不存在无符号数。
Java 位操作符一共有七个:&、|、^、~、<<、>>、>>>。

第一个是按位与;
第二个是按位或;
第三个是按位异或
第四个是按位非;
第五个是左移位;
第六个是右移位;
第七个是无符号右移位。

<<      :     左移运算符,num << 1,相当于num乘以2;

>>      :     右移运算符,num >> 1,相当于num除以2,有符号右移,如果是负数,高位补1;

>>>    :     无符号右移,忽略符号位,空位都以0补齐;


4,字符串问题
1)务必考虑空串的情况。
2)匹配类问题
常用算法总结 - Garfield - 张广辉的博客
 
5,算法超时加速处理
1)求幂函数
常用算法总结 - Garfield - 张广辉的博客
 而不要这么写
常用算法总结 - Garfield - 张广辉的博客
 
2)通过使用Hash加速,因为Hash读写速度快
3)尽量少的访问集合或数组,用中间变量存一下。
int temp=nums[mid];
        int max=temp;

4)很多一维序列的操作,可以考虑两个方向,不要总是从左向右这样一个方向考虑。比如two sum问题,利用有序性。
5)递归和迭代的转换,一维序列的递归思路还是很好搞的。可以看做退化的二叉树,从前序、中序、后序找思路。
比如求数组{1,1,2,3,5,8........},该数组可以从后往前递归求解,而不一定必须从前往后迭代求解。

6,DFS递归注意
1),如果用一个list或者array作为中间变量时,递归到子函数之前加的值务必在执行完子函数之后除去。如果递归过程需要传递和返回的值为基础数据类型,如int,不要使用Integer,而需要把该int值定义为成员变量,这样就不需要传递了。
2),与DP的选取问题,DFS是一种遍历方法,当需要返回所有结果的元素时,用回溯;而只要求返回有多少种方法或判断是否可行时,用DP,此时用dfs太慢了。

7,常用的重要函数总结
String->Integer    String.valueOf(int a);
Integer->String    Integer.parseInt(String s);

8,字符串模式匹配
传统暴力算法 O(m*n)
常用算法总结 - Garfield - 张广辉的博客
 
KMP算法  O(m+n)
常用算法总结 - Garfield - 张广辉的博客
 
 
http://v.ku6.com/show/96kCpprZV8sEzXlKnGySAg...html
0 0
原创粉丝点击