算法
是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。对于同一个问题的解决,可能会存在着不同的算法,为了衡量一个算法的优劣,提出了空间复杂度与时间复杂度这两个概念。
时间复杂度
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间度量记为 ** T(n) = O(f(n)) **,它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。这里需要重点理解这个增长率。
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
二、查找算法
查找和排序是最基础也是最重要的两类算法,熟练地掌握这两类算法,并能对这些算法的性能进行分析很重要,这两类算法中主要包括二分查找、快速排序、归并排序等等。
顺序查找
顺序查找又称线性查找。它的过程为:从查找表的最后一个元素开始逐个与给定关键字比较,若某个记录的关键字和给定值比较相等,则查找成功,否则,若直至第一个记录,其关键字和给定值比较都不等,则表明表中没有所查记录查找不成功,它的缺点是效率低下。
二分查找
二分查找又称折半查找,对于有序表来说,它的优点是比较次数少,查找速度快,平均性能好。
二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x。
1、冒泡排序的基本思想是:设排序序列的记录个数为n,进行n-1次遍历,每次遍历从开始位置依次往后比较前后相邻元素,这样较大的元素往后移,n-1次遍历结束后,序列有序。
最佳情况下冒泡排序只需一次遍历就能确定数组已经排好序,不需要进行下一次遍历,所以最佳情况下,时间复杂度为** O(n) **。
最坏情况下冒泡排序需要n-1次遍历,第一次遍历需要比较n-1次,第二次遍历需要n-2次,...,最后一次需要比较1次,最差情况下时间复杂度为** O(n^2) **。
2、简单选择排序的思想是:设排序序列的记录个数为n,进行n-1次选择,每次在n-i+1(i = 1,2,...,n-1)个记录中选择关键字最小的记录作为有效序列中的第i个记录。
简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况** 无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为 O(n^2) ,进行移动操作的时间复杂度为 O(n) 。总的时间复杂度为 O(n^2) **。
最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。
简单选择排序是不稳定排序。
3、直接插入的思想是:是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增1的有序表。
最好情况下,当待排序序列中记录已经有序时,则需要n-1次比较,不需要移动,时间复杂度为** O(n) 。最差情况下,当待排序序列中所有记录正好逆序时,则比较次数和移动次数都达到最大值,时间复杂度为 O(n^2) 。平均情况下,时间复杂度为 O(n^2) **。
4、希尔排序又称“缩小增量排序”,它是基于直接插入排序的以下两点性质而提出的一种改进:(1) 直接插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。(2) 直接插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序属于插入类排序,是将整个有序序列分割成若干小的子序列分别进行插入排序。
排序过程:先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。
5、归并排序是分治法的一个典型应用,它的主要思想是:将待排序序列分为两部分,对每部分递归地应用归并排序,在两部分都排好序后进行合并。
归并排序的时间复杂度为O(nlogn),它是一种稳定的排序,java.util.Arrays类中的sort方法就是使用归并排序的变体来实现的。
6、快速排序的主要思想是:在待排序的序列中选择一个称为主元的元素,将数组分为两部分,使得第一部分中的所有元素都小于或等于主元,而第二部分中的所有元素都大于主元,然后对两部分递归地应用快速排序算法。
在快速排序算法中,比较关键的一个部分是主元的选择。在最差情况下,划分由n个元素构成的数组需要进行n次比较和n次移动,因此划分需要的时间是O(n)。在最差情况下,每次主元会将数组划分为一个大的子数组和一个空数组,这个大的子数组的规模是在上次划分的子数组的规模上减1,这样在最差情况下算法需要(n-1)+(n-2)+...+1= ** O(n^2) **时间。
最佳情况下,每次主元将数组划分为规模大致相等的两部分,时间复杂度为** O(nlogn) **。
7、堆排序之前首先需要了解堆的定义,n个关键字序列K1,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):(1) ki <= k(2i)且 ki <= k(2i+1) (1 ≤ i≤ n/2),当然,这是小根堆,大根堆则换成>=号。
如果将上面满足堆性质的序列看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有的非终端节点的值均不大于(或不小于)其左右孩子节点的值。
堆排序的主要思想是:给定一个待排序序列,首先经过一次调整,将序列构建成一个大顶堆,此时第一个元素是最大的元素,将其和序列的最后一个元素交换,然后对前n-1个元素调整为大顶堆,再将其第一个元素和末尾元素交换,这样最后即可得到有序序列。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序时间复杂度也为O(nlogn),空间复杂度为O(1)。它是不稳定的排序方法。与快排和归并排序相比,堆排序在最差情况下的时间复杂度优于快排,空间效率高于归并排序。
递归算法
程序调用自身的编程技巧称为递归。
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
分治算法
分治算法的思想是将待解决的问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后合并这些子问题的解来建立最终的解。分治算法中关键地一步其实就是递归地求解子问题。
分治法解题的一般步骤:
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
动态规划
动态规划与分治方法相似,都是通过组合子问题的解来求解待解决的问题。但是,分治算法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,而动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。动态规划方法通常用来求解最优化问题。
动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。
动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
贪婪算法(Greedy algorithm)是一种对某些求最优解问题的更简单、更迅速的设计技术。用贪婪法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。
基本思路
⒈建立数学模型来描述问题。
⒉把求解的问题分成若干个子问题。
⒊对每一子问题求解,得到子问题的局部最优解。
⒋把子问题的解局部最优解合成原来解问题的一个解。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解。
回溯算法
回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。用回溯算法解决问题的一般步骤为:
1、定义一个解空间,它包含问题的解。
2、利用适于搜索的方法组织解空间。
3、利用深度优先法搜索解空间。
4、利用限界函数避免移动到不可能产生解的子空间。
问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。
拆半插入排序
/**
* 拆半插入排序(二分插入排序):是直接插入排序改良版,也需要i-1趟插入,不同之处在于,第i趟插入,先找到插入的位置
*
*
**/
public class BinaryInsertSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
binzryInsertSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void binzryInsertSort(int[] data){
if(data==null||data.length==0) return;
for(int i=1;i<data.length;i++){
if(data[i]<data[i-1]){
int temp = data[i];
int low = 0;
int high = i-1;
while(low<=high){
int mid = (low+high)/2;
if(data[mid]<temp){
low = mid+1;
}else{
high = mid-1;
}
}
for(int j =i;j>low;j--){
data[j]=data[j-1];
}
data[low]=temp;
}
}
}
}
二.泡排序
/**
* 冒泡排序:将每个元素视为气泡,遵循轻的气泡不能在重的之下,不断循环将轻的浮到上面
* 时间复杂度:O(n2)
* 空间复杂度:O(1)
* 算法稳定,冒泡算法有许多特性和插入排序算法相似,但是系统开销更大
*
**/
public class BubbleSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
bubbleSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void bubbleSort(int[] data){
if(data == null || data.length==0) return ;
for (int i = 0;i<data.length;i++) {//每次循环一定能将待排序数组中的最大值交换到最后
boolean flag = false;
for (int j = 0;j<data.length-i-1;j++){//这样这里的j就没有必要循环到data.length
if(data[j]>data[j+1]){
CommonSort.swagData(data,j,j+1);
flag=true;
}
}
if(!flag) break;//如果完成一侧循环发现数组是有序的就没有必要继续循环了
}
}
}
三.直接选择排序
public class DirectSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
selectSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void selectSort(int[] data){
if(data==null||data.length==0) return;
for (int i = 0;i<data.length;i++) {
int min = data[i];
for(int j=i+1;j<data.length;j++){
if(data[j]<data[i]){
CommonSort.swagData(data,i,j);
}
}
}
}
}
四.堆排序
/**
* 堆排序:大根堆排序/小根堆排序,每次为待排序数据创建新堆,然后将新堆的第一项与最后一项交换(待排序)
* 不断的调整堆直到堆中只有一个元素
* 时间复杂度:O(nlog2n)
* 空间复杂度:O(1)
* 不稳定,不推荐使用
*
**/
public class HeapSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
heapSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void heapSort(int[] data){
if(data==null||data.length==0) return;
for(int i=0;i<data.length;i++){
int len=data.length-1-i;
createNewHeap(data,len);
CommonSort.swagData(data,0,len);
}
}
private static void createNewHeap(int[] data, int lastIndex) {
for (int i =(lastIndex-1)/2;i>=0;i--){
int k =i;
while(k*2+1<=lastIndex){
int bigIndex = 2*k+1;
if(bigIndex<lastIndex){
if(data[bigIndex]<data[bigIndex+1]){
bigIndex++;
}
}
if(data[k]<data[bigIndex]){
CommonSort.swagData(data,k,bigIndex);
k=bigIndex;
}else{
break;
}
}
}
}
}
五.直接插入排序
/**
* 直接插入排序
*
*
**/
public class InsertSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
insertSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void insertSort(int[] data){
if(data == null||data.length==0) return;
for(int i= 1;i<data.length;i++){
int temp = data[i];
if(data[i]<data[i-1]){
int j = i-1;
//将有序数组整体后移,找到temp的位置进行赋值
while(j>=0&&data[j]>temp){
data[j+1]=data[j];
j--;
}
data[j+1] =temp;
}
}
}
}
六.快速排序
/**
* 快速排序:从元素中随便找一个元素作为分界值,循环遍历将比其小的放到其左边,比其打的放到其右边,
* 接下来对左右两个子序列依照上面规则进行循环遍历,直到每个子序列中只有一个元素
* 时间复杂度:O(nlog2n)
*
**/
public class QuickSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
quickSort(data,0,data.length-1);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void quickSort(int[] data,int start,int end){
if(start >end) return;//很关键,控制选取的分界点数据为最大或最小时直接跳出
int piv = data[start];//选取分界点
int i = start+1;//开始循环位置
int j = end;
while(i<=j){
while(i<=end&&data[i]<piv){//从左向右开始一直找一个比分界点小的数据
i++;
}
while(j>=start&&data[j]>piv){//从右向左寻找比分界点大的数据
j--;
}
if(i<j){
CommonSort.swagData(data,i,j);
}
}
CommonSort.swagData(data,start,j);
quickSort(data,start,j-1);
quickSort(data,j+1,end);
}
}
七.希尔排序
/**
* 希尔排序:是对直接插入排序的一种改进
*
*
**/
public class ShellSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
shellSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void shellSort(int[] data){
//计算出最大h
int h =1;
while(h<data.length/3){
h=h*3+1;
}
while(h>0){
for(int i = h;i<data.length;i+=h){
if(data[i]<data[i-h]){
int temp=data[i];
int j = i-h;
while(j>=0&&data[j]>temp){
data[j+h]=data[j];
j-=h;
}
data[j+h]=temp;
}
}
h=(h-1)/3;
}
}
}
是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。对于同一个问题的解决,可能会存在着不同的算法,为了衡量一个算法的优劣,提出了空间复杂度与时间复杂度这两个概念。
时间复杂度
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间度量记为 ** T(n) = O(f(n)) **,它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。这里需要重点理解这个增长率。
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
二、查找算法
查找和排序是最基础也是最重要的两类算法,熟练地掌握这两类算法,并能对这些算法的性能进行分析很重要,这两类算法中主要包括二分查找、快速排序、归并排序等等。
顺序查找
顺序查找又称线性查找。它的过程为:从查找表的最后一个元素开始逐个与给定关键字比较,若某个记录的关键字和给定值比较相等,则查找成功,否则,若直至第一个记录,其关键字和给定值比较都不等,则表明表中没有所查记录查找不成功,它的缺点是效率低下。
二分查找
二分查找又称折半查找,对于有序表来说,它的优点是比较次数少,查找速度快,平均性能好。
二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x。
二分查找的时间复杂度为O(logn)
三、排序算法1、冒泡排序的基本思想是:设排序序列的记录个数为n,进行n-1次遍历,每次遍历从开始位置依次往后比较前后相邻元素,这样较大的元素往后移,n-1次遍历结束后,序列有序。
最佳情况下冒泡排序只需一次遍历就能确定数组已经排好序,不需要进行下一次遍历,所以最佳情况下,时间复杂度为** O(n) **。
最坏情况下冒泡排序需要n-1次遍历,第一次遍历需要比较n-1次,第二次遍历需要n-2次,...,最后一次需要比较1次,最差情况下时间复杂度为** O(n^2) **。
2、简单选择排序的思想是:设排序序列的记录个数为n,进行n-1次选择,每次在n-i+1(i = 1,2,...,n-1)个记录中选择关键字最小的记录作为有效序列中的第i个记录。
简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况** 无关。当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为 O(n^2) ,进行移动操作的时间复杂度为 O(n) 。总的时间复杂度为 O(n^2) **。
最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。
简单选择排序是不稳定排序。
3、直接插入的思想是:是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增1的有序表。
最好情况下,当待排序序列中记录已经有序时,则需要n-1次比较,不需要移动,时间复杂度为** O(n) 。最差情况下,当待排序序列中所有记录正好逆序时,则比较次数和移动次数都达到最大值,时间复杂度为 O(n^2) 。平均情况下,时间复杂度为 O(n^2) **。
4、希尔排序又称“缩小增量排序”,它是基于直接插入排序的以下两点性质而提出的一种改进:(1) 直接插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。(2) 直接插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序属于插入类排序,是将整个有序序列分割成若干小的子序列分别进行插入排序。
排序过程:先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。
5、归并排序是分治法的一个典型应用,它的主要思想是:将待排序序列分为两部分,对每部分递归地应用归并排序,在两部分都排好序后进行合并。
归并排序的时间复杂度为O(nlogn),它是一种稳定的排序,java.util.Arrays类中的sort方法就是使用归并排序的变体来实现的。
6、快速排序的主要思想是:在待排序的序列中选择一个称为主元的元素,将数组分为两部分,使得第一部分中的所有元素都小于或等于主元,而第二部分中的所有元素都大于主元,然后对两部分递归地应用快速排序算法。
在快速排序算法中,比较关键的一个部分是主元的选择。在最差情况下,划分由n个元素构成的数组需要进行n次比较和n次移动,因此划分需要的时间是O(n)。在最差情况下,每次主元会将数组划分为一个大的子数组和一个空数组,这个大的子数组的规模是在上次划分的子数组的规模上减1,这样在最差情况下算法需要(n-1)+(n-2)+...+1= ** O(n^2) **时间。
最佳情况下,每次主元将数组划分为规模大致相等的两部分,时间复杂度为** O(nlogn) **。
7、堆排序之前首先需要了解堆的定义,n个关键字序列K1,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):(1) ki <= k(2i)且 ki <= k(2i+1) (1 ≤ i≤ n/2),当然,这是小根堆,大根堆则换成>=号。
如果将上面满足堆性质的序列看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有的非终端节点的值均不大于(或不小于)其左右孩子节点的值。
堆排序的主要思想是:给定一个待排序序列,首先经过一次调整,将序列构建成一个大顶堆,此时第一个元素是最大的元素,将其和序列的最后一个元素交换,然后对前n-1个元素调整为大顶堆,再将其第一个元素和末尾元素交换,这样最后即可得到有序序列。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序时间复杂度也为O(nlogn),空间复杂度为O(1)。它是不稳定的排序方法。与快排和归并排序相比,堆排序在最差情况下的时间复杂度优于快排,空间效率高于归并排序。
递归算法
程序调用自身的编程技巧称为递归。
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
分治算法
分治算法的思想是将待解决的问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后合并这些子问题的解来建立最终的解。分治算法中关键地一步其实就是递归地求解子问题。
分治法解题的一般步骤:
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
动态规划
动态规划与分治方法相似,都是通过组合子问题的解来求解待解决的问题。但是,分治算法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,而动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。动态规划方法通常用来求解最优化问题。
动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。
动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
贪婪算法(Greedy algorithm)是一种对某些求最优解问题的更简单、更迅速的设计技术。用贪婪法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。
基本思路
⒈建立数学模型来描述问题。
⒉把求解的问题分成若干个子问题。
⒊对每一子问题求解,得到子问题的局部最优解。
⒋把子问题的解局部最优解合成原来解问题的一个解。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解。
回溯算法
回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。用回溯算法解决问题的一般步骤为:
1、定义一个解空间,它包含问题的解。
2、利用适于搜索的方法组织解空间。
3、利用深度优先法搜索解空间。
4、利用限界函数避免移动到不可能产生解的子空间。
问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。
拆半插入排序
/**
* 拆半插入排序(二分插入排序):是直接插入排序改良版,也需要i-1趟插入,不同之处在于,第i趟插入,先找到插入的位置
*
*
**/
public class BinaryInsertSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
binzryInsertSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void binzryInsertSort(int[] data){
if(data==null||data.length==0) return;
for(int i=1;i<data.length;i++){
if(data[i]<data[i-1]){
int temp = data[i];
int low = 0;
int high = i-1;
while(low<=high){
int mid = (low+high)/2;
if(data[mid]<temp){
low = mid+1;
}else{
high = mid-1;
}
}
for(int j =i;j>low;j--){
data[j]=data[j-1];
}
data[low]=temp;
}
}
}
}
二.泡排序
/**
* 冒泡排序:将每个元素视为气泡,遵循轻的气泡不能在重的之下,不断循环将轻的浮到上面
* 时间复杂度:O(n2)
* 空间复杂度:O(1)
* 算法稳定,冒泡算法有许多特性和插入排序算法相似,但是系统开销更大
*
**/
public class BubbleSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
bubbleSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void bubbleSort(int[] data){
if(data == null || data.length==0) return ;
for (int i = 0;i<data.length;i++) {//每次循环一定能将待排序数组中的最大值交换到最后
boolean flag = false;
for (int j = 0;j<data.length-i-1;j++){//这样这里的j就没有必要循环到data.length
if(data[j]>data[j+1]){
CommonSort.swagData(data,j,j+1);
flag=true;
}
}
if(!flag) break;//如果完成一侧循环发现数组是有序的就没有必要继续循环了
}
}
}
三.直接选择排序
public class DirectSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
selectSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void selectSort(int[] data){
if(data==null||data.length==0) return;
for (int i = 0;i<data.length;i++) {
int min = data[i];
for(int j=i+1;j<data.length;j++){
if(data[j]<data[i]){
CommonSort.swagData(data,i,j);
}
}
}
}
}
四.堆排序
/**
* 堆排序:大根堆排序/小根堆排序,每次为待排序数据创建新堆,然后将新堆的第一项与最后一项交换(待排序)
* 不断的调整堆直到堆中只有一个元素
* 时间复杂度:O(nlog2n)
* 空间复杂度:O(1)
* 不稳定,不推荐使用
*
**/
public class HeapSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
heapSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void heapSort(int[] data){
if(data==null||data.length==0) return;
for(int i=0;i<data.length;i++){
int len=data.length-1-i;
createNewHeap(data,len);
CommonSort.swagData(data,0,len);
}
}
private static void createNewHeap(int[] data, int lastIndex) {
for (int i =(lastIndex-1)/2;i>=0;i--){
int k =i;
while(k*2+1<=lastIndex){
int bigIndex = 2*k+1;
if(bigIndex<lastIndex){
if(data[bigIndex]<data[bigIndex+1]){
bigIndex++;
}
}
if(data[k]<data[bigIndex]){
CommonSort.swagData(data,k,bigIndex);
k=bigIndex;
}else{
break;
}
}
}
}
}
五.直接插入排序
/**
* 直接插入排序
*
*
**/
public class InsertSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
insertSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void insertSort(int[] data){
if(data == null||data.length==0) return;
for(int i= 1;i<data.length;i++){
int temp = data[i];
if(data[i]<data[i-1]){
int j = i-1;
//将有序数组整体后移,找到temp的位置进行赋值
while(j>=0&&data[j]>temp){
data[j+1]=data[j];
j--;
}
data[j+1] =temp;
}
}
}
}
六.快速排序
/**
* 快速排序:从元素中随便找一个元素作为分界值,循环遍历将比其小的放到其左边,比其打的放到其右边,
* 接下来对左右两个子序列依照上面规则进行循环遍历,直到每个子序列中只有一个元素
* 时间复杂度:O(nlog2n)
*
**/
public class QuickSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
quickSort(data,0,data.length-1);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void quickSort(int[] data,int start,int end){
if(start >end) return;//很关键,控制选取的分界点数据为最大或最小时直接跳出
int piv = data[start];//选取分界点
int i = start+1;//开始循环位置
int j = end;
while(i<=j){
while(i<=end&&data[i]<piv){//从左向右开始一直找一个比分界点小的数据
i++;
}
while(j>=start&&data[j]>piv){//从右向左寻找比分界点大的数据
j--;
}
if(i<j){
CommonSort.swagData(data,i,j);
}
}
CommonSort.swagData(data,start,j);
quickSort(data,start,j-1);
quickSort(data,j+1,end);
}
}
七.希尔排序
/**
* 希尔排序:是对直接插入排序的一种改进
*
*
**/
public class ShellSort {
public static void main(String[] args){
int[] data = new int[]{1,2,4,8,-1,3,-10,-4};
System.out.println("排序前的数据:"+ Arrays.toString(data));
shellSort(data);
System.out.println("排序后的数据:"+Arrays.toString(data));
}
public static void shellSort(int[] data){
//计算出最大h
int h =1;
while(h<data.length/3){
h=h*3+1;
}
while(h>0){
for(int i = h;i<data.length;i+=h){
if(data[i]<data[i-h]){
int temp=data[i];
int j = i-h;
while(j>=0&&data[j]>temp){
data[j+h]=data[j];
j-=h;
}
data[j+h]=temp;
}
}
h=(h-1)/3;
}
}
}