通过算法分析来引导如何通过代码来实现心中的想法,如何去写程序,实现算法。
2.Advanced algorithm for solving
2.1 divide and merge
方法: 分治法求解问题的思想是将问题中通过整体代入的思想将问题递归地分成小问题(即规模较小的原问题),并且每一次递归后都要进行合并操作,只有这样在递归结束后,分解的小问题的解就自动合并成最终的解。
注:分治法分为:只分,先分后治,先治后分。
2.1.2 quickSort 先治后分
Problem:Given an array of integers nums, sort the array in ascending order?
Askfor: 以最少的时间和最少的空间实现
Solution:
方法1 采用基于分治思想的快速排序进行求解。
快速排序思想:
-
- 治:选取其中一个数为标杆,将小于它的的数全部赋值摆放到其左边,将大于它的数全部赋值摆放到其右边。目的就是将该标杆数摆放至与最终排序结果相一致的位置。
- 分:在将该数组以标杆数位置为中线,对其左边的数进行(1.1)操作,如果左边还有数的话;然后对其右边的数进行(1.1)操作,如果右边还有数的话。因此划分递归的终止条件就是标杆位置的左右两边都没有数了,为止。
那么根据以上的治分思想,如何进行编程实现了?
首先,根据先治后分的想法,可写出快排算法的整体基本框架:
//*********************************************************
void quickSort(vector<int> &num,int low, int high)
{
int part = divideLR(num,low,high); //治:flag表示当前标杆,返回中间数字位置。
if (low<part)
{
quickSort(num, low, part - 1); // 左分
}
if (high>part)
{
quickSort(num, part + 1, high); // 又分
}
}
//*********************************************************
然后,需要寻找一组数中的标杆数,可以直接选取每次需要排序数(子数)的第一个数,或最后一个数,或者中间位置上的一个数,你也可以自己想一个设定标杆数的方法。(标杆数的位置不同可以导致时间复杂度不同,但平均时间复杂度是相同的)
下面,我们选取每次递归的中间位置的数作为标杆:在选取标杆数的同时,也在将数进行左右的划分,小的划到左边,大的划到右半边。治方法的基本实现变成下面:
int divideLR( vector<int>&num, int low, int high)
{
int flag = (low + high) / 2; //计算当前部分的标志位
int temp = num[flag]; //存储标志位数据
int t = 0;
}
Low 和high 表示当前递归阶段需要进行快排的位置范,注意:num不管递归到哪里,始终是整个数组,因为传递的是别名。
接下来是快速排序最重要的部分,如何根据当前标志位的数对左右的数进行划分呢?
因为数都是杂乱无序的,所以待排序中的每一个数都必须被遍历并与当前的标志位比较到。所以一次递归的时间复杂度为O(n),每个数都要被遍历到,所以必须要有一个循环结构,已知的有for 和while,
如果用for(i)即仅有一个变量i的话,当比较到一个数比标志位数小的时候,我们自然可以不用管它,而直接i++;但当比较到一个数比标志位数大的时候,我们感觉无从下手将该数移动到标志位数的右边。
这时,我们想到在右边也增加一个j--进行向左遍历,这样可把i的数赋值给j,想一想为什么不是交换i和j位置的数呢?
通过for(i=low,j=high;i<j;)实现了遍历,但是本文采用了while循环,即while(low<high),两种方法都可以,循环只是一种往返重复的方式,不同循环语句本质上没有区别!
因此,在程序中使用while,治方法的最终实现变成了下面:
int divideLR( vector<int>&num, int low, int high)
{
int flag = (low + high) / 2;
int temp = num[flag]; //存储标志位数据
int t = 0;
while (low < high)
{
}
}
根据上面的治理论,如何通过编码实现while循环中数据的划分。首先想到的是通过low++ , high--来遍历,那么while循环可这样写:
While(low<high)
{
if(num[high]>temp)
{
high--;
}
else
{
if(num[low]>temp)
{
exchange(num[high],num[low]);
}
}
if(num[low]<temp)
{
low++;
}
else
{
if(num[high]<=temp)
exchange(num[low],num[high]);
}
return low; //low的左边小于temp;low的右边则大于或等于temp,所以low是一次循环最后返回位置;
}
根据标志位数,左右分可直接写出以上基本程序。接下来,就要考虑所有的边界条件:正负数、存在0的数据、存在重复数等是否也可正确运行出结果?
运行程序最后变成下面:
//*********************************************
void quickSort(vector<int> &num, int low, int high)
{
//每一次递归的的中间标杆都不一样,需要计算,这里与归并排序不同的是这里采用先治后分
int part = divideLR(num, low, high); //治flag表示当前标杆,返回中间数字位置。
if (low<part)
{
quickSort(num, low, part - 1); // 左分
}
if (high>part)
{
quickSort(num, part+1, high); // 又分
}
}
int divideLR(vector<int>&num, int low, int high)
{
int flag = (low + high) / 2;
int temp = num[flag]; //存储标志位数据
while (low<high)
{
if (num[high]>temp)
{
//if (low < high)
//{
high--;
//}
}
else
{
if (num[low] > temp) //交换的原则是比较停下来
{
int t = num[low];
num[low] = num[high];
num[high] = t;
}
}
if (num[low] < temp)
{
low++;
}
else
{
if (num[high] <= temp) //若相等,则交换
{ //注这里为什么要加<=而不是< , 如果直接同理上面写小于的话,结果好像也是正确的,其实最后的问题是如何处理重复数,处理重复数整整花了半天时间?后面会继续讲到如何处理重复数!
int t = num[low];
num[low] = num[high];
num[high] = t;
}
}
}
return low;
}
输出结果:
(1)非重复数可以输出正确结果如下:
(2)当存在重复数字时如8,5,8,9 则出现死循环。
总结:没有必要处理等于号
3,4,5,5,6,5,7 当num[low]=num[high]=temp时,就会出现死循环,为了跳出死循环,当时最直接想到的办法就是通过high--来破除这个死循环;而为什么没有对low++,根据对称性,随后经过验证,low++也是可以的。这样做只有一个目的就是保证比temp大的数一定在low
的右边。
那么下面这个判断语句写在哪里呢?
if (num[high] == num[low]) //若交换的两个数也相等,
{
low++; //或者high--;
}
写法1. 最开始的一种尝试怎么处理当num[high]==temp时,我们也进行与low交换,这样就考虑到了与temp相等的情况,但是当low==high时,会出现总在交换这两个数,即死循环;接下来在if (num[high] <=temp)中写了跳出相等情况的代码,结果实验结果是正确的,如下图:
if (num[low] < temp)
{
if (low < high)
{
low++;
}
}
else
{
if (num[high] <=temp) //若相等,则交换
{
int t = num[low];
num[low] = num[high];
num[high] = t;
if (num[high] == num[low]) //若交换的两个数也相等,
{
high--;
}
}
}
然后根据对称性将其写入
if (num[low] > temp) //交换的原则是比较停下来
{
int t = num[low];
num[low] = num[high];
num[high] = t;
if (num[high] == num[low]) //当,8,5,8,9时可以跳出循环,但没有考虑==temp的情况,而以上结果可以是考虑到了等于temp情况。
{
low++;
}
}
发现结果是死循环,原因是当==temp时即,8,8,8,9时也会出现死循环,所以当写在if (num[low] > temp) 里面时,必须考虑==temp情况,才可以避免low==high==temp==8的情况。
那么从整体while里面的结构来看,如果所有的if都不考虑==temp的情况,即最后的num[high]==num[low],我们可以在以上两个if判断完后,在对只对num[high]==num[low]做一个判断即可(注这个想法在随后的实验中被证明是错误的,应该必须仅仅考虑==temp的情况),因为,本质上是不用考虑等于temp的情况,因为不管等不等于,只要满足low位置比temp大或等于的数都在右边,而小或者等于的数都在左边,最后返回low,注意,low的位置的数是否一定==temp了???
按照程序设定:是的,low是否可以大于temp了,如果是的话,在前面应该被交换了,而如果小的话,就直接low++。所以最后low的位置的数是等于temp,最终返回正确的low的位置。
最后,重复数到底在哪边,取决于最后num[low]==num[high]时,处理low 或high的情况,如果是low++,则是low跳过temp,那么最后与temp相等的数就应该在low的左边;如果是high--,那么与temp相等的数应该出现在low的右边,不管出现在哪边,最后的排序,如右边与原temp相等的数会被不断的递归排序而排在第一个位置。所以两边拼接在一起的时候相等的数就连在一起:下面是实验验证结果:当采用low++来处理重复数时,
总方程下一轮递归中采用low+1 和low-1 而没有考虑等于low的情况,因此当存在low的右边存在比low小的时候,
注如果最后low==temp 并且low==high时,我们继续low++,那么最终返回的low就不是指向temp,这样依旧得到low的左边和右边符合小大条件,但是当前low所指的数是不确定的,是一个比temp大很多以及比其右边的数大很多的数,而在下一次递归中采用的是low+1,而没有考虑,最后导致这个low的数没有随之而考虑,导致下一轮排序的错误;而采用high--;则可以避免该错误的发生,因此即便high越界,最终返回的是正确low指向temp.
总结:
- 经过不断的实验和论证,最后在处理重复数时,如果返回的是low,那么采用high--,从而保证返回low不会因为最后的++而产生没有指向temp的情况。
- 处理重复数,最后递归到处理low==high==temp,注意必须是等于temp的情况,因为如果只有low==high!=temp时,即便不考虑最后的high--,程序仍然可继续指向,不会出现死循环,实验证明只有当三种同时相等时才会出现死循环。最后必须考虑==temp的情况,如果不考虑,那么返回的结果就是low==high!=temp,而最后low++,这样返回的结果不是指向temp标杆值了,除非采用high--(如总结1),则可以避免这种情况的发生,但要low 和high都可以的话,就必须考虑等于temp。
- 有两种处理重复数的方法:以下是两个形式不同但本质原理相同(如总结2)的代码实现。至此通过总结(2)的原理,彻底想通了两种方法都可以的原因。其实最终的最后还是回归到了程序最初的设计原则:返回的下标值符合左右划分,而要满足这个条件必须返回的是temp值的下标,因为整个程序就是根据temp来进行判断的,所以只有temp值的下标才保证了正确的左右划分。
//*****************************************************
//完全实现方法1
void DivideMerge::quickSort(vector<int> &num, int low, int high)
{
//每一次递归的的中间标杆都不一样,需要计算,这里与归并排序不同的是这里采用先治后分
int part = divideLR(num, low, high); //治flag表示当前标杆,返回中间数字位置。
if (low<part)
{
quickSort(num, low, part - 1); // 左分
}
if (high>part)
{
quickSort(num, part+1, high); // 又分
}
}
int DivideMerge::divideLR(vector<int>&num, int low, int high)
{
int flag = (low + high) / 2;
int temp = num[flag]; //存储标志位数据
while (low<high)
{
if (num[high]>temp)
{
high--;
}
else
{
if (num[low] > temp) //交换的原则是比较停下来
{
int t = num[low];
num[low] = num[high];
num[high] = t;
}
}
if (num[low] < temp)
{
low++;
}
else
{
if (num[high] <temp) //若相等,则交换
{
int t = num[low];
num[low] = num[high];
num[high] = t;
}
}
if (num[high] == num[low]&&num[low]==temp) //若交换的两个数也相等,
{
high--; }
}
return low;
}
//*****************************************************
//完全实现方法2
void DivideMerge::quickSort(vector<int> &num, int low, int high)
{
//每一次递归的的中间标杆都不一样,需要计算,这里与归并排序不同的是这里采用先治后分
int part = divideLR(num, low, high); //治flag表示当前标杆,返回中间数字位置。
if (low<part)
{
quickSort(num, low, part - 1); // 左分
}
if (high>part)
{
quickSort(num, part+1, high); // 又分
}
}
int DivideMerge::divideLR(vector<int>&num, int low, int high)
{
int flag = (low + high) / 2;
int temp = num[flag]; //存储标志位数据
while (low<high)
{
if (num[high]>temp)
{
high--;
}
else
{
if (num[low] > temp) //交换的原则是比较停下来
{
int t = num[low];
num[low] = num[high];
num[high] = t;
}
}
if (num[low] < temp)
{
low++;
}
else
{
if (num[high] <=temp) //若相等,则交换
{
int t = num[low];
num[low] = num[high];
num[high] = t;
if (num[high] == num[low]) //注意是放在if里面,三者相等原则
{
high--;
}
}
}
}
return low;
}
// goto