基于复杂问题求解策略设计的排序算法

来源:互联网 发布:河北网络干部学院下载 编辑:程序博客网 时间:2024/06/06 14:17

一:常见的复杂问题求解策略以及由其产生的算法和常见问题有:
递归与分治:大规模问题转换为小规模问题,最后要有一个边界。直接插入排序+归并排序+快速排序
动态规划:分阶段处理,多阶段决策。前面的阶段都对后面的阶段产生影响。
Floyd算法+冒泡排序
穷举(回溯+分支限界):枚举所有的可能,在枚举过程中,根据verify判定是否在当前状态基础上继续枚举。
水仙花 N皇后 马踏棋盘
贪心思想:最小生成树,Huffman树,选择排序
逆向思维:Hash查找 基数排序
以退为进:堆排序
二:基于递归与分治设计的直接插入排序和归并排序
1、直接插入排序
1.1思路
根据递归与分治的思想,首先第一个首记录一定有序,然后从第二个到最后一个,每次都将当前记录插入到其前面有序表中,最终使所有记录有序。
1.2代码实现
比较待插入记录和他的前一个记录,如果大于等于前一个记录,则不用移动。否则,就备份带插入记录,将他前面的元素分别后移,直到备份记录大于他前面的记录为止。最后将备份记录填入即可。但这种表述存在一些问题,就是可能会出现越界的情况,为了避免这种情况的发生,我们引入了哨所的概念,即在存记录的时候浪费一个空间,将a[0]用于备份记录。这样就可以将上面的表述改成,比较待插入记录和他的前一个记录,如果大于等于前一个记录,则不用移动。否则,将待插入记录存入哨中,只要之前的元素大于哨中的元素就后移。最后将哨中记录填入即可。这样因为不用每次都判断是否越界,比较时用的时间(不是复杂度)相对少些。因为这个操作比较简单实用,下面只给出用哨进行插入排序的代码。

void InsertSort(int* a,int N){    for(int i=2;i<=N;i++)    {        if(a[i]<a[i-1])//比较        {            a[0]=a[i];//移动            int j;            for(j=i-1;a[j]>a[0];j--)            {          //比较                a[j+1]=a[j];//移动            }            a[j+1]=a[0];//移动        }    }}

1.3复杂度分析
最好情况:待排序序列关键字非递减有序(正序)
“比较”的次数:n-1;“移动”的次数 0
最坏情况:待排序序列关键字非递增有序(逆序)
“比较”的次数:(n-1)(n+2)/2;“移动”的次数 (n+4)(n-1)/2
时间复杂度O(n^2),空间复杂度O(1),稳定!!!
2、折半插入排序
2.1思路
因为插入排序有一个非常重要的操作,就是在前面有序的子序列找到第一个比带插入元素大的元素,所以我们可以用折半查找来定位,如此实现的插入排序我们便称为折半插入排序。
2.2代码实现
low=1,high=i-1,mid=(low+high)/2;与中间元素比较,如果中间元素大,则high=mid-1,fo否则,low=mid+1。重复上述操作,当low第一次大于high时,high右侧是第一个比带插入元素严格大的元素,插入元素为high+1(也就是low)。

void midInsertSort(int *a,int N){    for(int i=2;i<=N;i++)    {        a[0]=a[i];        int low=1,high=i-1;        while(low<=high)        {            int mid=(low+high)/2;            if(a[mid]>a[0]) high=mid-1;                else                low=mid+1;        }        for(int j=i-1;j>high;j--)        {            a[j+1]=a[j];        }        a[low]=a[0];    }}

2.3复杂度分析
时间复杂度:O(n^2);空间复杂度:O(1);稳定!
3、希尔排序
3.1思路
首先根据希尔增量,将记录序列划分为d个子序列,分别对各个子序列进行直接插入排序,最后一趟d=1时对全部记录进行直接插入排序。又称缩小增量排序。(注:虽然这种排序算法也体现了以退为进的思想,但由于他调用了多次插入排序,故将他一并放入了这个模块里面)这个算法设计是由于充分洞察了直接插入排序的优点,即序列基本有序的时候直接插入排序很快,对更短的子序列进行排序是,插入排序也很快。这种算法就是将直接插入排序的优点发挥到最大,有效提升了排序的速度。
3.2代码实现
根据上面的思路跟容易给出希尔排序的代码,其中最主要的就是希尔增量的确定。使用不同的增量对希尔排序的时间复杂度的改进将不一样,甚至一点小的改变都将引起算法性能剧烈的改变。现在好像也没有确定的希尔增量选取的方法。可以看下下面链接所提供的论文:希尔排序最佳增量序列研究
https://wenku.baidu.com/view/0ffd354bcf84b9d528ea7ae3.html
下面的代码是根据k=N/2的增量序列给出的代码:

int* getDldt(int N,int* dldk,int & k){    k=0;    while(N!=0)    {        dldk[k++]=N/2;        N/=2;    }    return dldk;}void shellInsert(int* a,int dk,int N){    for(int i=dk+1;i<=N;i++)    {        if(a[i]<a[i-dk])          {              a[0]=a[i];              int j;            for(j=i-dk;j>0&&(a[0]<a[j]);j-=dk)                a[j+dk] = a[j];  //记录后移一增量         a[j+dk] = a[0];          }    }}void shellSort(int *a,int *dldk,int k,int N){    for(int i=0;i<k;i++)    shellInsert(a,dldk[i],N);}

3.3复杂度分析
子序列排好序后大序列基本有序,此时直接插入插入排序效率高。此外,初始是对更短的子序列排序,插入排序的效率也很高。时间是d序列的函数,而d的确定尚待研究,最坏时间复杂度为O(n^2),实验最快会达到O(n^1.3)。是一个不稳定的排序算法。比如:11 23 12 9 18 16 25 36 30 47 30 第二趟希尔排序,增量设为3时后面的30会跑到前一个30的钱前面。
4、其他插入排序
2-路插入排序
表插入排序
呃…我也不会,只是看到鲁大师课件中给出了,就先列这里以后有时间再学习吧。
5、归并排序
5.1思路
递归与分治:递归边界:长度为一的自然有序,一分为二,分别排序,然后归并。
5.2代码实现
5.2.1用递归的思想进行实现,只要理清递归关系,写出一个归并函数进行递归即可。

void Merge(int R[],int low,int mid,int high,int T[]){    int P_Left=low,P_Right=mid+1,P_Result=low;    while(P_Left<=mid&&P_Right<=high)    {        if(R[P_Left]<=R[P_Right])            {            T[P_Result]=R[P_Left];            P_Left++;P_Result++;            }        else            {                T[P_Result]=R[P_Right];                 P_Right++;P_Result++;            }    }     while(P_Left<=mid)     {          T[P_Result]=R[P_Left];            P_Left++;P_Result++;     }     while(P_Right<=high)     {         T[P_Result]=R[P_Right];         P_Right++;P_Result++;     }   for(int i=low;i<P_Result;i++)   {       R[i]=T[i];   }//将中间数组的值赋到原数组的对应位置}//将两个有序的序列归并一个有序的序列,跟求并集一样。void MSort(int R[],int low,int high,int Temp[]){    if(low<high){        int mid=(low+high)/2;        MSort(R,low,mid,Temp); //递归公式        MSort(R,mid+1,high,Temp);//递归公式        Merge(R,low,mid,high,Temp);    }}

5.2.2非递归实现
因为单个元素一定有序,不用进行划分,直接两两进行归并,直到最后只有两个归并块为止。
5.3复杂度分析
时间复杂度:O(NlogN);空间复杂度:O(N);稳定!
6、快速排序
6.1思路
快速排序也是基于分治思想提出的,找一个“杠杆”,杠杆左边都比“杠杆”元素”小”,右边都比“杠杆”元素“大”,
然后对“杠杆”左边和右边分别进行递归即可。
6.2代码实现
有最左侧元素做“枢轴”,存入“哨所”。(枢轴元素的选择会影响算法的速度)设low和high指向两端,high向左移动,一旦遇到小于枢纽的元素,则将其移动到左侧,放入low指向的位置;low向右移动,一旦遇到大于枢轴的元素,则将其移动到右侧,放入high指向的位置;high和low如此移动下去,直到high==low,记录下枢轴的位置,此时杠杆左边都比“杠杆”元素”小”,右边都比“杠杆”元素“大”。

int Partition(int *a,int low,int high){    a[0]=a[low];    while(low<high)    {        while(high>low&&a[high]>=a[0])//防止在找的过程中high==low            high--;        a[low]=a[high];        while(low<high&&a[low]<=a[0])            low++;        a[high]=a[low];    }    a[low]=a[0];    return low;}void Qsort(int *a,int low,int high){    if(low<high)    {        int Partition_=Partition(a,low,high);        Qsort(a,low,Partition_-1);        Qsort(a,Partition_+1,high);    }}

6.3复杂度分析
平均复杂度:O(NlogN) 平均最快(因为不要脸,不跟基数排序玩….23333)
最坏时间复杂度:O(N^2);
平均空间复杂度:O(logN);
最坏空间复杂度:O(N);
改进:枢轴在两侧与中间中择一。
不稳定!!!
三:基于贪心思想设计的选择排序算法
1、简单选择排序
1.1思路
每一趟选择出当前最小的交换到最前面,至多N-1趟排序完成。
1.2代码实现
void selectSort(int *a,int N)
{
for(int i=1;i<=N-1;i++)
{
int minn=i;
for(int j=i+1;j<=N;j++)
{
if(a[minn]>a[j])
minn=j;
}
if(minn!=i)
{
int temp=a[minn];
a[minn]=a[i];
a[i]=temp;
}
}
}
1.3复杂度分析
时间复杂度:O(n^2);
空间复杂度O(1);
不稳定!!!
1.4不稳定的原因分析
举个栗子,3、3*、3**、2。将这一组数从小到大排列,三后面的* 用来标记三出现的先后顺序。按照上面给出的选择排序代码,第一次找最小的数的时候,将2跟3交换,原序列中第一个3在排完序之后成了最后一个。所以是不稳定,但这种不稳定可以改进,下面便给出再时间复杂度和空间复杂度都不变的情况下,一个稳定的选择排序算法。
2、简单排序算法的稳定性改进
2.1思路
再找到最小元素的下标minn是,不直接将他与a[i]交换,而是在i+1与minn之间找与a[i]相等的元素,找到就让a[i]和找到的元素交换。这样,最多讲过n-i次交换就可以保证其a[i]成为离a[minn]最近并且与无序块首记录关键字相等的元素,此时再将a[i]和a[minn]交换,则排序算法保持稳定性。
2.2代码实现

void selectSort(int *a,int N){    for(int i=1;i<=N-1;i++)    {        int minn=i;        for(int j=i+1;j<=N;j++)        {            if(a[minn]>a[j])                minn=j;        }        if(minn!=i)        {            for(int k=i+1;k<minn;k++)            {                if(a[k]==a[i])                {                    int temp=a[k];                a[k]=a[i];                a[i]=temp;                }            }            int temp=a[minn];            a[minn]=a[i];            a[i]=temp;        }    }}

2.3复杂度分析
比简单排序稍慢,但从复杂度分析的角度,其时间复杂度并没有提高,都是O(n^2)。其空间复杂度也是O(1),也没有发生改变。
2.4稳定性分析
同样举个例子,3、3-、3–、2。将这一组数从小到大排列,三后面的- 用来标记三出现的先后顺序。按照上面的排序中算法,在与无序块首记录关键字相等的元素交换的过程中,第一次得到的序列是 3-、3、3–、2,第二次得到的序列是,3–、3、3- 、2。最后再将2和3–交换,得到的就是2、3、3-、3–这个序列,可以看到,按照改进的排序算法并没有改变原序列中想等元素的顺序,所以改进的选择排序算法是稳定的!!!
具体参考论文http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFDLAST2016&filename=RJDK201602023&uid=WEEvREcwSlJHSldRa1FhcTdWZDhML2VlZDlxK0FHL2ZORXFRR1pZL3ZxND0=
四:基于动态规划设计的冒泡排序算法
1、动态规划的思想
问题分解:全局问题分解为局部问题。
迭代求解:由初始阶段逐步递推,前一阶段影响后一阶段知道最终得到全局解。
2、冒泡排序
2.1思路
每一趟排序,每一躺排序时,从首记录开始相邻两数进行比较,逆序则交换,每趟排序都会使当前“最大”的数“沉到末尾”,小数则逐步“上升”,重复n-1趟排序完成。怎么改进?这样会出现重复操作的情况,即在中间的某一趟已经排序完成了,但他仍会执行后面无意义的操作。那么怎么判断他已经排序完成了呢,即在中间的某趟没有发生交换既证明排序已经结束。
2.2代码实现

void bubbleSort(int *a,int N){    for(int i=1;i<N;i++)     {         int flag=0;         for(int j=1;j<=N-i;j++)    {        int temp;        if(a[j+1]<a[j])        {            flag=1;            temp=a[j+1];            a[j+1]=a[j];            a[j]=temp;        }    }    if(flag==0)        break;     }}

2.3复杂度分析
最好的情况:序列开始就有序的情况。时间复杂度O(n);
最坏的情况:序列逆序的情况。时间复杂度O(n^2);
这两种情况的空间复杂度都为O(1);
稳定!
五:基于逆向思维设计的基数排序算法
1、思路
所有排序都必须要有的一个原子操作就是就是比较,那如何不比较也能排序呢?这种不比较也能排序的算法就是基数排序,基数排序的提出是循序渐进的,他是先解决了一个最简单的问题,即关键字不重复,取值范围有限,即分配排序。然后解决了一个稍微复杂的问题即,关键字可重复,取值范围有限,即桶排序。最后便是关键字可重复,取值范围大,这里采用的解决办法是多趟分配与收集,每一趟范围小。在我看来,这里也体现了动态规划的思想。
2、代码实现
从个位数开始分别进行分配和收集,每趟的分配都存到队列数组中,然后顺序收集。
下面的代码是对int型正数进行排序,如果有负数要单独处理。

 for(int i=1;i<=10;i++)   {       Distribute(a,S,i,N);       collect(a,S);   } void Distribute(int *a,LinkQueue* S,int k,int N) {     int temp;     for(int i=0;i<N;i++)     {    temp=a[i];         for(int j=1;j<k;j++)         {             temp=temp/10;         }         temp%=10;         EnQueue(S[temp],a[i]);     } } void collect(int *a,LinkQueue* S) {     int j=0;     for(int i=0;i<10;i++)     {         while(S[i].front_!=S[i].rear_)            DeQueue(S[i],a[j++]);     } }

3、复杂度分析
时间复杂度:O(n+radix) ;
空间复杂度 : O(n+radix);
稳定!!!漂亮啊!!!
六:基于以(zhen)退(long)为(qi)进(ju)思想设计的堆排序
1、堆排序的引入
第一趟选择极值元素时,能否多作些工作,为后面选择极值元素带来便利呢。
2、堆排序
2.1思路
小顶堆对应的完全二叉树中任意结点均比其孩子小或者相等;大顶堆对应的完全二叉树中任意结点均比其孩子大或相等。
小顶堆堆顶(首元素)最小,大顶堆堆顶最大,子树也是堆。
基本思想:利用堆每次选择出最大的交换到末尾。
具体方法:
(1)先建初始大顶堆;
(2)堆顶与堆尾互换(最大记录换到最后);忽略堆尾,将前N-1个记录组成的树重新调整成大顶堆;
(3)重复上一步N-1趟即可
2.2代码实现
Floyd在实现堆排序的过程中也运用到了以退为进的思想,先假设得到了初始大顶堆,即先解决了第二步筛选后,运用第二问的结论调整建堆建立初始大顶堆。
2.2.1筛选的实现
备份堆顶,较堆顶与其两个孩子,若堆顶不是最大的则将两个孩子中大的“上移”,之后的子树可能不再是堆,重复上述操作至最后(当前堆顶大于等于孩子或无孩子)
2.2.2建立初始大顶堆
从最后一个非叶子结点向根结点方向逐步调整建堆。

void HeapSort(int *a,int N){    for(int i=N/2;i>0;i--) //第一个非叶子结点是N/2    HeapAdjust(a,i,N);    for(int i=N;i>1;i--)    {        int temp=a[1];        a[1]=a[i];        a[i]=temp;         HeapAdjust(a,1,i-1);    }}void HeapAdjust(int *a,int k,int N){    a[0]=a[k];    for(int i=2*k;i<=N;i*=2)//i*2变成i对应的左孩子    {        if(i<N&&a[i]<a[i+1])//避免i==N的情况            i++;//i始终指向指向大孩子        if(a[0]>=a[i]) break;        else        {        a[k]=a[i];        k=i;        }    }    a[k]=a[0];}

2.3复杂度分析
最坏时间复杂度:O(nlogn);
空间复杂度:O(1);
不稳定!!!
———————————————————–分界线——————————————————–
鲁大师好多课件都有的一句话:狡黠者鄙读书,无知者羡读书,惟明智之士用读书,然书并不以用处告人,用书之智不在书中,而在书外,全凭观察得之。共勉!!!