18.数据结构与算法-排序

155 阅读19分钟

内排序:是在排序整个过程中,待排序的所有记录全部被放置在内存中;

外排序:由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行

冒泡排序(Bubble Sort)

一种交换排序,它的基本思想就是: 两两比较相邻的记录的关键字,如果反序则交换,直到没有反序的记录为⽌.

数据结构设计&简单函数

#define MAXSIZE 15
typedef struct
{
    //用于存储要排序数组,r[0]用作哨兵或临时变量
    int r[MAXSIZE+1];
    //用于记录顺序表的长度
    int length;
}SqList;


//2.排序常用交换函数实现
//交换L中数组r的下标为i和j的值
void swap(SqList *L,int i,int j)
{
    int temp=L->r[i];
    L->r[i]=L->r[j];
    L->r[j]=temp;
}

//3.数组打印
void print(SqList L)
{
    int i;
    for(i=1;i<L.length;i++)
        printf("%d,",L.r[i]);
    printf("%d",L.r[i]);
    printf("\n");
}

冒泡排序初级版本: 对顺序表L进行交换排序

// 带哨兵
void BubbleSort0(SqList *L){
   
    int i,j;
    for (i = 1; i < L->length; i++) {
        for (j = i+1; j <= L->length; j++) {
            if(L->r[i] > L->r[j])
                swap(L, i, j);
        }
    }
    
}
// 不带哨兵
void BuddleBase(SqList *L){
    for (int i = 0; i<L->length; i++) {
        for (int j = i+1; j<L->length; j++) {
            if (L->r[i] > L->r[j]) {
                swap(L, i, j);
            }
        }
    }
}

正宗冒泡排序算法

void BubbleSort(SqList *L){
    int i,j;
    for (i = 1; i < L->length; i++) {
        //注意:j是从后面往前循环
        for (j = L->length-1; j>=i; j--) {
            
            //若前者大于后者(注意与上一个算法区别所在)
            if(L->r[j]>L->r[j+1])
                //交换L->r[j]与L->r[j+1]的值;
                swap(L, j, j+1);
        }
    }
}

冒泡排序优化

void BubbleSort2(SqList *L){
    int i,j;
    //flag用作标记
    Status flag = TRUE;
    
    //i从[1,L->length) 遍历;
    //如果flag为False退出循环. 表示已经出现过一次j从L->Length-1 到 i的过程,都没有交换的状态;
    for (i = 1; i < L->length && flag; i++) {
        
        //flag 每次都初始化为FALSE
        flag = FALSE;
        
        for (j = L->length-1; j>=i; j--) {
            
            if(L->r[j] < L->r[j+1]){
            //交换L->r[j]和L->r[j+1]值;
            swap(L, j, j+1);
            //如果有任何数据的交换动作,则将flag改为true;
            flag=TRUE;
            }
        }
//        if (flag == FALSE) {//表明表中记录排序完成
//            break;
//        }
    }
}

//非哨兵
/*
 origin:  9,1,5,8,3,7,4,6,2
 
 9,1,5,8,3,7,4,2,6
 9,1,5,8,3,7,2,4,6
 9,1,5,8,3,2,7,4,6
 9,1,5,8,2,3,7,4,6
 9,1,5,2,8,3,7,4,6
 9,1,2,5,8,3,7,4,6
 1,9,2,5,8,3,7,4,6
 */
void BuddleBase11(SqList *L){
    int flag = 1;
    for (int i = 0; i<L->length; i++) {
        flag = 0;
        printf("====== 第%d轮 ======\n",i);
        for (int j = L->length-2; j>=i; j--) {
            if(L->r[j] < L->r[j+1]){
                printf("r[j]: %d, r[j+1]: %d", L->r[j], L->r[j+1]);
                printf("\n");
                swap(L, j, j+1);
                flag = 1;
            }
        }
        if (flag == 0) {//表示已经排序好了,不用继续后面的循环了
            break;
        }
    }
}

简单选择排序算法(Simple Selection Sort)

就是通过n-i次关键词比较,从n-i+1个记录中找出关键字最小的记录,并和第i(1<=i<=n) 个记录进行交换

/*
 {9,1,5,8,3,7,4,6,2};
 1,9,5,8,3,7,4,6,2  交换: 9 1
 1,2,5,8,3,7,4,6,9  交换: 9 2
 1,2,3,8,5,7,4,6,9  交换: 5 3
 1,2,3,4,5,7,8,6,9  交换: 8 4
 1,2,3,4,5,6,8,7,9  交换: 7 6
 1,2,3,4,5,6,7,8,9  交换: 8 7
 */
//7.选择排序--对顺序表L进行简单选择排序
void SelectSort(SqList *L){
    
    int i,j,min;

    for (i = 1; i < L->length; i++) {
        //① 将当前下标假设为最小值的下标
        printf("===== 第%d轮======\n",i);
        min = i;
        //② 循环比较i之后的所有数据
        for (j = i+1; j <= L->length; j++) {
            //③ 如果有小于当前最小值的关键字,将此关键字的下标赋值给min
            if (L->r[min] > L->r[j]) {
                min = j;
            }
        }
        
        //④ 如果min不等于i,说明找到了最小值,则交换2个位置下的关键字
        if(i!=min)
            printf("交换: %d %d\n",L->r[i],L->r[min]);
            swap(L, i, min);
    }
}

直接插入排序算法(Stright Insertion Sort)

基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表;

空间复杂度: O(1) 解读:在直接插入排序中只使用了i,j,temp这三个辅助元素,与问题规模⽆无关,空间复杂度为O(1)
时间复杂度: O(n2)

/*
 {9,1,5,8,3,7,4,6,2};
 
 9 1 5 8 3 7 4 6 2
 1 9 5 8 3 7 4 6 2
 1 5 9 8 3 7 4 6 2
 1 5 8 9 3 7 4 6 2
 
 */
//直接插入排序算法--对顺序表L进行直接插入排序
void InsertSort(SqList *L){
    int i,j;
    //L->r[0] 哨兵 可以把temp改为L->r[0] 记录待插入值
    int temp=0;
    
    //假设排序的序列集是{0,5,4,3,6,2};
    //i从2开始的意思是我们假设5已经放好了. 后面的牌(4,3,6,2)是插入到它的左侧或者右侧
    for(i=2;i<=L->length;i++)
    {
        //需将L->r[i]插入有序子表
        if (L->r[i]<L->r[i-1])
        {
            //设置哨兵 可以把temp改为L->r[0]
            temp = L->r[i];
            for(j=i-1; L->r[j]>temp && j>0; j--){
                    //记录后移
                printf("%d %d\n",L->r[j], L->r[j+1]);
                    L->r[j+1]=L->r[j];
            }
            
            //插入到正确位置 可以把temp改为L->r[0]
            L->r[j+1]=temp;
        }
    }
}

// 不带哨兵
void InsertSort_noguard(int a[], int n)
{
    for(int i= 1; i<n; i++){
        if(a[i] < a[i-1]){//若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
            int j= i-1;
            int x = a[i];
            while(j>-1 && x<a[j]){  //采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
                a[j+1] = a[j];
                j--;
            }
            a[j+1] = x;      //插入到正确位置
        }
    }
}

希尔排序

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排 序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减⾄至1时,整个序列列恰被分成一组,算法便终止.

步长序列 最坏情况下时间复杂度
n / 2i O(n2)
2k -1 O(n3/2)
2i3i O(nlog2n)
//希尔排序-对顺序表L希尔排序 (可看成优化的插入排序)
void shellSort(SqList *L){
    int i,j;
    int increment = L->length;
    
    //9,1,5,8,3,7,4,6,2
    //① 当increment 为1时,表示希尔排序结束
    do{
        //② 增量序列 自定义 暂无最好的增量序列(世界难题)
        increment = increment/3+1; //4
        //③ i的待插入序列数据 [increment+1 , length]
        for (i = increment+1; i <= L->length; i++) {
            //④ 如果r[i] 小于它的序列组元素则进行插入排序,例如3和9. 3比9小,所以需要将3与9的位置交换
            if (L->r[i] < L->r[i-increment]) {
                //⑤ 将需要插入的L->r[i]暂时存储在L->r[0].和插入排序的temp 是一个概念;
                L->r[0] = L->r[i];
                
                //⑥ 记录后移
                for (j = i-increment; j > 0 && L->r[0]<L->r[j]; j-=increment) {
                    L->r[j+increment] = L->r[j];
                }
                
                //⑦ 将L->r[0]插入到L->r[j+increment]的位置上; 上面循环后 j-=increment
                L->r[j+increment] = L->r[0];
            }
        }
    }while (increment > 1);
}

堆排序

是具有下面性质的完全二叉树:

每个结点的值都大于或等于其左右孩子结点的值, 称为⼤大顶堆; 
或者每个结点的值都小于等于其左右孩子的结点的值,称为小顶堆

堆排序(Heap Sort) 就是利用堆进行排序的算法(一般升序采用大顶堆,降序采用小顶堆).

基本思想:

1. 将待排序的序列构成一个大顶堆,此时,整个序列的最大值就堆顶的根结点,将 它“沉”到数组末端(其实就是将其与堆数组的末尾元素交换, 此时末尾元素就是最大值);
2. 然后将剩余的n-1个序列重新构成一个堆,这样就会得到n个元素的次大值, 如此重复执行,就能得到一个有序序列了

堆排序的时间复杂度为:O(nlogn) 堆排序是就地排序,空间复杂度为常数:O(1)

//大顶堆调整函数 将序列变成大顶堆
/*
 条件: 在L.r[s...m] 记录中除了下标s对应的关键字L.r[s]不符合大顶堆定义,其他均满足;
 结果: 调整L.r[s]的关键字,使得L->r[s...m]这个范围内符合大顶堆定义.
 */
/*
 9,1,5,8,3,7,4,6,2
 */
void HeapAjust(SqList *L,int s,int m){
    
    int temp,j;
    //① 将L->r[s] 存储到temp ,方便后面的交换过程;
    temp = L->r[s];
    
    //② j 为什么从2*s 开始进行循环,以及它的递增条件为什么是j*2
    //因为这是颗完全二叉树,而s也是非叶子根结点. 所以它的左孩子一定是2*s,而右孩子则是2s+1;(二叉树性质5)
    for (j = 2 * s; j <=m; j*=2) {
        
        //③ 判断j是否是最后一个结点, 并且找到左右孩子中最大的结点;
        //如果左孩子小于右孩子,那么j++; 否则不自增1. 因为它本身就比右孩子大;
        if(j < m && L->r[j] < L->r[j+1])
            ++j;
        
        //④ 比较当前的temp 是不是比较左右孩子大; 如果大则表示我们已经构建成大顶堆了;
        if(temp >= L->r[j])
            break;
        
        //⑤ 将L->[j] 的值赋值给非叶子根结点
        L->r[s] = L->r[j];
        printf("%d %d\n", temp, L->r[j]);
        //⑥ 将s指向j; 因为此时L.r[4] = 60, L.r[8]=60. 那我们需要记录这8的索引信息.等退出循环时,能够把temp值30 覆盖到L.r[8] = 30. 这样才实现了30与60的交换;
        s = j;
    }
    
    //⑦ 将L->r[s] = temp. 其实就是把L.r[8] = L.r[4] 进行交换;
    L->r[s] = temp;
}


//10.堆排序--对顺序表进行堆排序
void HeapSort(SqList *L){
    int i;
   
    //1.将现在待排序的序列构建成一个大顶堆;
    //将L构建成一个大顶堆;
    //i为什么是从length/2.因为在对大顶堆的调整其实是对非叶子的根结点调整.
    for(i=L->length/2; i>0;i--){
        HeapAjust(L, i, L->length);
    }
    
    
    //2.逐步将每个最大的值根结点与末尾元素进行交换,并且再调整成大顶堆
    for(i = L->length; i > 1; i--){
        
        //① 将堆顶记录与当前未经排序子序列的最后一个记录进行交换;
        swap(L, 1, i);
        //② 将L->r[1...i-1]重新调整成大顶堆;
        HeapAjust(L, 1, i-1);
    }
}

归并排序(Merging Sort)

利用归并的思想实现排序方法. 它的原理理是假设初始序列含有n个记录,则可以看成n个有序的子序列. 每个⼦列的长度为1,然后两合并.得 到[n/2]个长度为2或1的有序子序列, 再两两归并. ......如此重复,直到得到一个长度为n 的有序列为此. 这种排序⽅法称为2路归并排序

递归

//11.归并排序-对顺序表L进行归并排序 {50,10,90,30,70,40,80,60,20}
//③ 将有序的SR[i..mid]和SR[mid+1..n]归并为有序的TR[i..n]
// SR:待合并数组 TR:合并好的  i:低位下标 m:中间位下标 n:高位下标
// 相当于合并2个有序数组,第一个数组下标是[i...m],第二个下标是[m+1...n]
void Merge(int SR[],int TR[],int i,int m,int n)
{
    int j,k,l; //k是TR数组下标  j是第二个数组开始下标
    //1.将SR中记录由小到大地并入TR
    for(j=m+1,k=i;i<=m && j<=n;k++)
    {
        if (SR[i]<SR[j])
            TR[k]=SR[i++];
        else
            TR[k]=SR[j++];
    }
    
    //2.将剩余的SR[i..mid]复制到TR
    if(i<=m)
    {
        for(l=0;l<=m-i;l++)
            TR[k+l]=SR[i+l];
    }
    
    //3.将剩余的SR[j..mid]复制到TR
    if(j<=n)
    {
        for(l=0;l<=n-j;l++)
            TR[k+l]=SR[j+l];
    }
}


//② 将SR[s...t] 归并排序为 TR1[s...t]; {50,10,90,30,70,  40,80,60,20}
// SR是带排序数组, TR1是排序好返回数组,low是低位下标 hight高位下标
void MSort(int SR[],int TR1[],int low, int hight){
    int mid;
    int TR2[MAXSIZE+1];//临时存放的数组
    
    if(low == hight)
        TR1[low] = SR[low];
    else{
        //1.将SR[low...hight] 平分成 SR[low...mid] 和 SR[mid+1,hight];
        mid = (low + hight)/2;
        //2. 递归将SR[low,mid]归并为有序的TR2[low,mid];
        MSort(SR, TR2, low, mid);//递归完这个 才会走下一个递归
        //3. 递归将SR[mid+1,hight]归并为有序的TR2[mid+1,hight];
        MSort(SR, TR2, mid+1, hight);
        //4. 将TR2[low,mid] 与 TR2[mid+1,hight], 归并到TR1[low,hight]中
        Merge(TR2, TR1, low, mid, hight);
    }
}

//① 对顺序表L进行归并排序
void MergeSort(SqList *L){
   
    MSort(L->r,L->r,1,L->length);
}

时间复杂度: 一趟归并需要将 SR[1]~SR[n]中相邻长度为h的有序序列的进行两两比较. 并将结果存储在TR1[1]- TR2[n]. 这需要将待排序的所有记录扫描一遍. 因此耗费 O(n) 时间. ⽽整个归并排序需要进行log2n 次. 因此总的时间复杂度为 O(nlogn).

空间复杂度: 由于递归排序在递归的过程中需要与原始记录序列同样数量的存储归并结果以及递归时深度为log2n 的栈空间; 因此空间复杂度为 O(n+log2n)

归并排序(非递归)

// 递归拆分规则:  由大到小
// 非递归拆分规则:由小到大
//12.归并排序(非递归)-->对顺序表L进行非递归排序
//对SR数组中相邻长度为s的子序列进行两两归并到TR[]数组中;
//50,10,90,30,70,40,80,60,20
void MergePass(int SR[],int TR[],int s,int length){
  
    int i = 1;
    int j;
    
    //①合并数组
    //s=1 循环结束位置:8 (9-2*1+1=8)
    //s=2 循环结束位置:6 (9-2*2+1=6)
    //s=4 循环结束位置:2 (9-2*4+1=2)
    //s=8 循环结束位置:-6(9-2*8+1=-6) s = 8时,不会进入到循环;
    while (i<= length-2*s+1) {
        //两两归并(合并相邻的2段数据)
        Merge(SR, TR, i, i+s-1, i+2*s-1);
        i = i+2*s;
        /*
         s = 1,i = 1,Merge(SR,TR,1,1,2);
         s = 1,i = 3,Merge(SR,TR,3,3,4);
         s = 1,i = 5,Merge(SR,TR,5,5,6);
         s = 1,i = 7,Merge(SR,TR,7,7,8);
         s = 1,i = 9,退出循环;
         */
        
        /*
         s = 2,i = 1,Merge(SR,TR,1,2,4);
         s = 2,i = 5,Merge(SR,TR,5,6,8);
         s = 2,i = 9,退出循环;
         */
        
        /*
         s = 4,i = 1,Merge(SR,TR,1,4,8);
         s = 4,i = 9,退出循环;
         */
    }
    
    //②如果i<length-s+1,表示有2个长度不等的子序列. 其中一个长度为length,另一个小于length
    //即数组是基数个,最后一个会等前面都排序好后,进行最后的合并
    // 1 < (9-8+1)(2)
    //s = 8时, 1 < (9-8+1)
    if(i < length-s+1){
        //Merge(SR,TR,1,8,9)
        Merge(SR, TR, i, i+s-1, length);
    }else{
        //③只剩下一个子序列; 50,10,90,30,  70,40,80,60,  20
        for (j = i; j <=length; j++) {
            TR[j] = SR[j];
        }
    }
}

void MergeSort2(SqList *L){
    int *TR = (int *)malloc(sizeof(int) * L->length);
    int k = 1;
    //k的拆分变换是 1,2,4,8;
    while (k < L->length) {
        //将SR数组按照s=2的长度进行拆分合并,结果存储到TR数组中;
        //注意:此时经过第一轮的归并排序的结果是存储到TR数组了;
        MergePass(L->r, TR, k, L->length);
        k = 2*k;
        //将刚刚归并排序后的TR数组,按照s = 2k的长度进行拆分合并. 结果存储到L->r数组中;
        //注意:因为上一轮的排序的结果是存储到TR数组,所以这次排序的数据应该是再次对TR数组排序;
        MergePass(TR, L->r, k, L->length);
        k = 2*k;
        
    }
}

快速排序(Quick Sort)

基本思想: 通过一趟排序将待排序记录分割成独立的两部分; 其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对两部分记 录继续进行排序,以达到整个排序有序的目的.

//选取当中一个关键字作为枢轴; 将它放在⼀一个合适的位置上,使得它的左边的值都它小,右边的值都比它大;这时枢轴的位置就是最终排序后枢轴的位置
/*
 Partition 函数的思路:
 选取第一个关键字作为枢轴;
 只要(low < high) 就循环持续的将表的两端进行交替向中间扫描 (两端交替循环)
 while 遍历从[low,high]的高端位置开始找,找到比枢轴大的关键字(高位调整循环)
    如找到,则修改范围. 将high递减;
    如果没找到,说明比枢轴小,则交换到低端位置 swap(L,low,high);
 while 遍历从[low,high]的低端位置开始找,找到比枢轴小的关键字(低位调整循环)
    如果找到,则修改范围,将low递增;
    如果没找到,说明比枢轴大,则交换到高端位置 swap(L,low,high);
 */
int Partition(SqList *L,int low,int high){
    int pivotkey;//记录枢轴
    //pivokey 保存子表中第1个记录作为枢轴记录;
    pivotkey = L->r[low];
    //① 从表的两端交替地向中间扫描;
    while (low < high) { //{50,10,90,30,70,40,80,60,20};
        //② 比较,从高位开始,找到比pivokey更小的值的下标位置;
        while (low < high &&  L->r[high] >= pivotkey)
            high--;
        //③ 将比枢轴值小的记录交换到低端;
        swap(L, low, high);
        //④ 比较,从低位开始,找到比pivokey更大的值的下标位置;
        while (low < high && L->r[low] <= pivotkey)
            low++;
        //⑤ 将比枢轴值大的记录交换到高端;
        swap(L, low, high);
        
    }
    
    //返回枢轴pivokey 所在位置; 此时 low==high
    return low;
}

//② 对顺序表L的子序列L->r[low,high]做快速排序;
/*
 1. 判断low是否小于 high;
 2. 求得枢轴,并且将数组枢轴左边的关键字都⽐它小, 右边的关键字都比枢轴对应的关键字大;
 3. 将数组一分为二,对低子表进行排序,对高子表进行排序;
 */
void QSort(SqList *L,int low,int high){
    int pivot;//
    
    if(low < high){
        //将L->r[low,high]一分为二,算出中枢轴值 pivot;
        pivot = Partition(L, low, high);
        printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
        //对低子表递归排序;
        QSort(L, low, pivot-1);
        //对高子表递归排序
        QSort(L, pivot+1, high);
    }
    
}

//① 调用快速排序(为了保证一致的调用风格)
void QucikSort(SqList *L){
    QSort(L, 1, L->length);
}

时间复杂度 最好情况: O(nlogn); 最坏情况O(n2)

空间复杂度取决于递归造成的栈空间;

  • 最好的情况下,递归树的深度为Log2n. 那么其空间复杂度为O(logn);
  • 最坏的情况下,需要进行n-1次递归调用,那么其空间复杂度为O(n);
  • 平均情况下时间复杂度为O(logn)

快速排序-优化

int Partition2(SqList *L,int low,int high){
   
    int pivotkey;
    
    /**1.优化选择枢轴**/
    //① 计算数组中间的元素的下标值; 10,50,90,30,70,40,80,60,20
    int m = low + (high - low)/2;
    //② 将数组中的L->r[low] 是整个序列中左中右3个关键字的中间值;
    //交换左端与右端的数据,保证左端较小;[9,1,5,8,3,7,4,6,2]
    if(L->r[low]>L->r[high])
        swap(L, low, high);
    //交换中间与右端的数据,保证中间较小; [2,1,5,8,3,7,4,6,9];
    if(L->r[m]>L->r[high])
        swap(L, high, m);
    //交换中间与左端,保证左端较小;[2,1,5,8,3,7,4,6,9]
    if(L->r[low]>L->r[m])
        swap(L, m, low);
    //交换后的序列:3,1,5,8,2,7,4,6,9
    //此时low = 3; 那么此时一定比选择 9,2更合适;
    
    
    /**2.优化不必要的交换**/
    //pivokey 保存子表中第1个记录作为枢轴记录;
    pivotkey = L->r[low];
    //将枢轴关键字备份到L->r[0];
    L->r[0] = pivotkey;
    
    //① 从表的两端交替地向中间扫描;
    while (low < high) {
        //② 比较,从高位开始,找到比pivokey更小的值的下标位置;
        while (low < high &&  L->r[high] >= pivotkey)
            high--;
        
        //③ 将比枢轴值小的记录交换到低端;
        //swap(L, low, high);
        //③ 采用替换的方式将比枢轴值小的记录替换到低端
        L->r[low] = L->r[high];
        
        //④ 比较,从低位开始,找到比pivokey更大的值的下标位置;
        while (low < high && L->r[low] <= pivotkey)
            low++;
        
        //⑤ 将比枢轴值大的记录交换到高端;
        //swap(L, low, high);
        //⑤ 采样替换的方式将比枢轴值大的记录替换到高端
        L->r[high] = L->r[low];
    }
    //将枢轴数值替换回L->r[low]
    L->r[low] = L->r[0];
    
    //返回枢轴pivokey 所在位置;
    return low;
}

//② 对顺序表L的子序列L->r[low,high]做快速排序;
#define MAX_LENGTH_INSERT_SORT 7 //数组长度的阀值
void QSort2(SqList *L,int low,int high){
    int pivot;
    //if(low < high){
    //当high-low 大于常数阀值是用快速排序;
    if((high-low)>MAX_LENGTH_INSERT_SORT){
        //将L->r[low,high]一分为二,算出中枢轴值 pivot;
        pivot = Partition2(L, low, high);//Partition ?
        printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
        //对低子表递归排序;
        QSort2(L, low, pivot-1);
        //对高子表递归排序
        QSort2(L, pivot+1, high);
    }else{
        //当high-low小于常数阀值是用直接插入排序
        InsertSort(L);
    }
}

//① 快速排序优化
void QuickSort2(SqList *L)
{
    QSort2(L,1,L->length);
}