内部排序算法总结

来源:互联网 发布:stm32与51单片机区别 编辑:程序博客网 时间:2024/05/22 00:10

内部排序算法总结

在整个排序过程中数据的主要部分存在外存储器,借助计算机的内存储器调整外存储器上的位置称为外部排序。而内部排序是指在排序的整个过程中,数据全部存放在计算机的内存储器,并且在内存储器调整数据位置。

(1)排序的关键字可以是整型、实型或字符串。
(2)如果排序不改变数据集合上具有相同值元素的相对前后顺序,则称该排序是稳定的。

一、简单的排序算法

气泡排序:

反复扫描数组,自下而上依次考察相邻两个记录。且扫描长度递减。

①气泡排序是稳定的。
②函数Sort():两个嵌套的for循环,第一个控制扫描终点即顶点(i=1; i<=n-1 ; i++),第二个控制指示变量位置(j=n; j>=i+1; j- -),比较A[ j ].key和A[ j-1 ].key。
③时间复杂性:内层循环时间为C1(ni) ,外层循环为C2(n)+n1i=1C1(ni)Cn2O(n2) 为时间复杂度。
④优化:利用一个标记量记录当前扫描有无交换操作,如果没有可直接跳出循环。可使最好情况下的时间复杂度为O(n)

/*****自上而下为元素A[1]~A[n]*****/void Sort(LIST A, int n){    int i, j, flag;    for(i=1; i<=n-1; i++){        for(j=n, flag=1; j>=i+1; j--)            if(A[j].key < A[j-1].key){                Swap(A[j], A[j-1]);                flag = 0;            }        if(flag)            return;    }}

插入排序:

向有序集合中不断插入元素至合适位置保持有序性不变。

①插入排序是稳定的。
②函数Sort():两个嵌套的循环,外层循环控制有序部分的扩大(i=1; i<=n; i++),内层循环寻找插入元素应该插入的位置(j=i; A[ j ].key < A[ j-1 ].key; j- -)。令A[0].key = -∞用于控制内层循环结束。
③时间复杂性:内层循环i次,i与n同阶;外层循环n-1次,时间为C1(n1)+ni=2i 。所以时间复杂度为O(n2)
④启发:有序集合初始为一个元素,然后逐渐向其中添加元素,所以单个元素是有序的。

void Sort(LIST A, int n){    int i, j;    A[0].key = INF;//INF定义为一个特别小的数    for(i=1; i<=n; i++)        for(j=i; A[j] < A[j-1]; j--)            Swap(A[j], A[j-1]);}

选择排序:

反复从无序集合中选择一个关键字最小的元素放入有序集合中。

①选择排序是稳定的。
②函数Sort():两个局部变量lowkey、lowindex存放当前选择的候选元素。两个嵌套的循环,外层循环控制有序部分的扩大(i=1; i<=n; i++),内层循环扫描无序集合,并逐个与候选元素比较,如果小于候选元素,就替换掉候选元素。内层循环结束后,候选元素即可走马上任。
③时间复杂性:内外循环次数均与n同阶,所以时间复杂度为O(n)
④与冒泡、插入的比较:前两个排序的Swap()次数平均为n44 ,而选择排序为n-1次,所以当元素很大时,选择排序更快。为减少调整元素的时间,数组可存放元素的指针。

void Sort(LIST A, int n){    keytype lowkey;    int lowindex;    int i, j;    for(i=1; i<=n; i++){        lowindex = i;//在A[i]~A[n]中选择最小元素换到A[i]处        lowkey = A[i].key;        for(j=i+1; j<=n; j++)            if(A[j] < lowkey){                lowindex = j                lowkey = A[j].key;            }        Swap(A[i], A[lowindex]);    }}

二、快速排序

快速排序不是稳定的排序。

基础:
(1)单个元素是有序的。
(2)局部有序则整体有序。

实际上快排的基础也是其后几个重要排序的基础。

方法:取一个基准元素v的值,使数组能够被分为两部分(通过内存数据交换),保证左边元素全小于v值,右边元素全大于等于v值。如果左右两部分都有序,则整体有序。然后将左右两部分视作两个规模更小的整体,进行分割交换。(分治)如此分解下去,最后可以分解为一个最小子问题,即左右均有且只有一个元素,因为单个元素是有序的,所以整体有序。

关键:①如何选取v值;②整体的分割和元素的交换

①选取v值的方法多种多样,现以一种为例:取v值为最左两个不同关键字的较大者。

函数FindPivot():局部变量firstkey保存v值的候选元素,初始化为A[ i ].key(整体从A[ i ]到A[ j ]),从左至右循环扫描数组一次,当找到不同于候选元素值的关键字时,如果大于firstkey则返回其下标,否则返回i;当数组所有元素值都相等时,则返回0,说明已经有序,无需分割交换。

②整体的分割和元素的交换:由于分割点位置,所以先交换,至达到整体已分为两部分为止(注意分割点并非一个元素,而是一个虚拟的分割线,而线右临元素并不一定为基准元素,由下面的扫描操作可知)。

a.扫描
引入两个指示变量 l,r,初始分别指向i,j 。先 l 向右移动,找到大于等于v值的关键字就停下;再 r 向左移动,找到小于v值的关键字就停下;交换A[ l ]和A[ r ],l++,r - -,此时 l 左边全部小于v值,r 右边全部大于v值。接下来判断 l 和 r 的位置关系。

b.测试
如果 l > r ( l 和 r 不会停在同一个位置),实际上就是 l = r+1,说明已经成功将整体分为了两部分,为A[ i ]~A[ l-1 ]、A[ r+1 ]~A[ j ] 。
如果 l < r,则A[ l ]和A[ r ]之间至少还有两个元素未处理,扫描继续。

函数Partition():初始化 l 和 r ,进入循环,交换上次扫描停下位置的两个元素,继续扫描直至 l 和 r 都停下,测试(是否 l < r),循环。对于第一次进入循环,会交换A[ i ]和A[ j ],但是没关系,因为如果A[ i ]小于A[ j ],则 l 和 r 第一次循环中不会移动,第二次循环开始时A[ i ]和A[ j ]会交换回来。

③函数QuickSort():局部变量有pivot存基准元素值,pivotindex存基准元素下标,k存较大集合序列的起始下标,用于递归两个子序列。首先找到基准元素的下标并赋给pivotindex(调用FindPivot());将v值赋给pivot;分割整体,将 l 值赋给k;递归调用Partition(),对序列A[ i ]~A[ k-1 ],A[ k ]~A[ j ]进行分割;直至pivotindex为零,有序,终止递归。

时间复杂性:平均时间复杂度为O(nlogn) ,最坏情况下的时间复杂度为O(n2) 。其中递归深入的层次决定算法的执行时间。为了减少递归深入的层次,使v值大小在序列中越居中,则划分出的两个子序列长度越接近,递归深度越浅。

//A为外部数组,不用传递。int FindPivot(int i, int j){    keytype firstkey;    int k;    firstkey = A[i].key;    for(k=i+1; k<=j; k++)        if(A[k].key < firstkey)            return i;        else if(A[k].key > firstkey)            return k;    return 0;}int Partition(int i, int j, keytype pivot){    int l, r;    l = i;    r = j;    do{        Swap(A[l], A[r]);        while(A[l].key < pivot)            l++;        while(A[r].key > pivot)            r--;    }while(l<r);    return l;}void QuickSort(int i, int j){    keytype pivot;    int pivotindex;    int k;    pivotindex = FindPivot(i, j);    if(pivotindex != 0){        pivot = A[pivotindex].key;        k = Partition(i, j, pivot);        QuickSort(i, k-1);        QuickSort(k, j);    }}

三、归并排序

归并排序是稳定的排序。

前面的排序思想是从整体出发,而归并排序是由局部走向整体,其基础和快排一致。

方法:首先将其分为n个有序的数据集合,每个数据集合有且只有一个元素。并且这些数据集合相邻。然后从左至右依次两个两个合为一组,且保证每组有序。合并方法为同时扫描两个有序序列,每次移动一位进行比较,选择较小者插入临时序列,继续扫描被取走元素的序列,另一个序列的指示变量不变。然后重复进行归并操作,直至归并为一个有序序列。当元素较大时,可以采用链表结构。

①单组合并函数Merge():对A[left], … ,A[mid]和A[mid+1], … ,A[right]合并,局部变量 i = left(第一个序列的指示变量),j = right(第二个序列的指示变量),k = left(临时序列B的指示变量)。一个循环同时扫描两个序列,并填充临时序列B。接着将未扫描完的序列剩余部分直接复制到临时序列B。

②整体归并函数Mpass():局部变量 i,用于划分组。循环单组合并每组序列,直至剩下未归并序列的总长度小于一组序列总长度时(i > n-2*len+1),循环结束。如果剩下未归并序列的总长度小于一个数据集合的长度(len)时,已经有序,则直接复制到B中;如果剩下未归并序列的总长度大于一个数据集合的长度时,则归并剩余两个集合,即Merge(i, i+len-1, n, A, B)。

③归并排序函数Sort():局部变量len,用于存当前数据集合长度,初始化为1;B为临时集合。进入循环,按当前数据集合长度进行整体归并,每次整体归并后,数据集合长度变为原来2倍。直至数据集合长度大于整个序列长度时,循环结束。

时间复杂性:对于有n个元素的数据集合,所有情况下均总共需要归并logn 编。而每次归并并排序的时间与n同阶,所以时间复杂度为O(nlogn)

void Merge(int left, int mid, int right, LIST A, LIST B){    int i = left;    int j = mid+1;    int k = left;    while(i<=mid && j<=right)        B[k++] = A[i].key <= A[j].key ? A[i++] : A[j++];    while(i<=mid)        B[k++] = A[i++];    while(j<=right)        B[k++] = A[j++];}void Mpass(int n, int len, LIST A, LIST B){    int i, k;    i = 1;    while(i <= (n-2*len+1)){        Merge(i, i+len-1, i+2*len-1, A, B);        i = i+2*len;    }    if((i+len-1) < n)        Merge(i, i+len-1, n, A, B);    else        for(k = i; k<=n; n++)            B[k] = A[k];}void Sort(LIST A,  int n){    int len = 1;    LIST B;    while(len<n){//一次循环进行两次归并操作,最终结果回到A,如果已经排好序,Mpass相当于复制操作。        Mpass(n, len, A, B);        len = 2*len;        Mpass(n, len, B, A);        len = 2*len;    }}

四、堆排序

堆排序是借助堆完全二叉树进行的,所用的堆为最小堆(小到大的顺序)。

利用数组A表示完全二叉树,则对于任一节点A[ i ],其左儿子为A[ 2*i ],其右儿子为A[ 2*i+1 ]

方法:首先将数组中所有元素整理成堆,然后每一次都取走堆根元素(与最后一个元素交换,接在数组尾端逆序依次存储),将最后一个元素放到堆根,再次将新二叉树(元素个数减一)整理成堆。重复之前操作,直至堆中只剩下一个元素,得到一个从大到小的序列。

①如何处理只有根节点不符合堆定义的完全二叉树成为堆?

函数PushDown():局部变量 r,用于指示最初的根节点元素的位置,初始化为first(假设整理的完全二叉树为A[first]~A[last])。进入循环和左右儿子比较,如果仅有一个左二子,若大于则交换,然后循环结束;如果左右儿子均存在,则与较小者比较,若大于则交换,指示变量移动,继续循环;如果左右儿子均不存在或者小于左右儿子,则循环结束。

②最开始如何建堆?:think about “单元素有序”,类似地,单元素的树也是堆。所以可以自底向上逐渐整理扩大堆。一个for循环就可以搞定。

③堆排序函数Sort():建堆 + 循环“取堆根元素,整理堆”

时间复杂性:堆排序由两部分组成,一个是建堆,一个是选择并整理。由于两部分都有Pushdown()操作,先分析此函数中的循环比较次数。Pushdown(first, last)中循环一次r至少变为原来的两倍,故循环次数与log(last/first) 同阶,而last/first与n同阶。所以PushDown()时间复杂度为O(logn) 。而建堆循环n/2次,排序循环n-1次,所以堆排序的时间复杂度为O(nlogn)

void PushDown(int first, int last){    int r = first;    while(r <= last/2){        if((r == last/2) && (last%2 == 0)){//r仅有一个左儿子,为最后一个元素            if(A[r].key > A[last].key)                Swap(A[r], A[last]);            r = last;        }        else if((A[2*r] <= A[2*r+1]) && (A[r] < A[2*r])){            Swap(A[r], A[2*r]);            r = 2*r;        }        else if((A[2*r+1] <= A[2*r]) && (A[r] < A[2*r+1])){            Swap(A[r], A[2*r+1]);            r = 2*r+1;        }        else            r = last;    }}void Sort(int n, LIST A){    int i;    for(i=n/2; i>=1; i--)//建堆        PushDown(i, n);    for(i=n; i>=2; i--){//选择并整理        Swap(A[1], A[i]);        PushDown(1, i-1);    }}

五、基数排序

与前述方法不同的是基数排序不是将关键字看成一个整体,而将其看成几个分量组成的数。根据分量的值进行排列;分量的考察顺序由分量的重要性决定。一般是从最低位开始,依次考察每个分量。

基数排序是稳定的排序。

方法:假设每个分量取值有0~9共10位,则先准备10个空队列,也可以叫做10个桶,考察每个分量时都要用这10个桶。现假定待排序的数据集合为序列A。

********* Q[ 0 ]:
********* Q[ 1 ]:
********* Q[ 2 ]:
********* Q[ 3 ]:
********* Q[ 4 ]:
********* Q[ 5 ]:
********* Q[ 6 ]:
********* Q[ 7 ]:
********* Q[ 8 ]:
********* Q[ 9 ]:

①考察第一个分量,即最低位(个位)时,将数组A中元素根据第一个分量大小依次扔进自己的桶中,最后从第一个桶至第十个桶依次取出桶内元素,顺序放回A中,每将一个桶取空后,再取下一个桶。最终只看序列A中第一个分量,是有序的。

②考察第二个分量,即次低位(十位)时,以同样方式根据第二个分量大小将之全部扔入桶中,最终每个桶中元素十位相同,个位有序。然后以同样方式从桶中将所有元素取回A中,此时只看序列A中的第一二个分量,是有序的。

③循环操作,直至最后一个分量也进行了分配与收集,A中的关键字整体是有序的。

④基数排序函数RadixSort():局部变量有Q[10],十个桶;data,作为分配收集的中间暂存变量;r,暂存关键字的分量值。如果有 i 个分量,循环 i 次,每次循环开始时,将队列置空,然后遍历A,将元素分配到桶中,然后遍历桶,将元素收集到A中。

时间复杂性:如果关键字有 i 个分量,则循环进行 i 次,每次分配和收集都要进行n次,一共2n次,所以时间复杂度为O(in) ,由于 i 为常数,所以基数排序时间复杂度为O(n)

优化:由于元素很大时,桶所占的存储空间也很大,同时复制操作也是十分耗时的,所以可以用链表实现基数排序,移动元素时只传送元素的指针,收集时将桶连接起来即可,最终Q[0]为整个链表。

void RadixSort(int figure, QUEUE &A)//figure表示分量个数{    QUEUE Q[10];    elementtype data;    int pass, r, i;    for(pass = 1; pass<=figure; pass++){        for(i=0; i<=9; i++)//置空桶            MakeNull(Q[i]);        while(!Empty(A)){//分配            data = Front(A);//取第一个元素            DeQueue(A);            r = Radix(data.key, pass);//获取分量值            EnQueue(data, Q[r]);        }        for(i=0; i<=9; i++)//收集            while(!Empty(Q[i])){                data = Front(Q[i]);//取第一个元素                DeQueue(Q[i]);                EnQueue(data, A);               }    }}/***************************//链表收集void Concatenate(QUEUE &Q1, QUEUE &Q2){    if(!Empty(Q2)){        Q1.rear->next = Q2.front->next;        Q1.rear = Q2.rear;    }}void RadixSort(int figure, QUEUE &A){    QUEUE Q[10];    elementtype data;    int pass, r, i;    for(pass = 1; pass<=figure; pass++){        for(i=0; i<=9; i++)            MakeNull(Q[i]);        while(!Empty(A)){            data = Front(A);            DeQueue(A);            r = Radix(data.key, pass);            EnQueue(data, Q[r]);        }        for(i=1; i<=9; i++)            Concatenate(Q[0], Q[i]);        A = Q[0];    }}***************************/
0 0
原创粉丝点击