数据结构与算法之排序算法

295 阅读13分钟

排序

定义

假设含有n个记录的序列为(r1,r2,.....,rn).其相应的关键字分别为{k1,k2,......,kn}.需确定 1,2,......,n的⼀种排序p1,p2,......pn.使其相应的关键字满⾜kp1<=kp2<=......<=kpn⾮递减(或⾮递增)关系.即使得到序列成为⼀个按关键字有序的序列(rp1,rp2,...,rpn).这样得出操作称为排序

排序的分类

  • 内排序:是在排序整个过程中,待排序的所有记录全部被放置在内存中;
  • 外排序:由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进⾏

排序的结构设计与交换函数实现

//1.排序算法数据结构设计//用于要排序数组个数最大值,可根据需要修改#define MAXSIZE 10000typedef 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;
}

冒泡排序(BubbleSort)

冒泡排序(BubbleSort)⼀种交换排序,它的基本思想就是:两两⽐较相邻的记录的关键字,如果反序则交换,直到没有反序的记录为⽌.

冒泡排序初级版本:


冒泡排序(BubbleSort)—完成形态



冒泡排序(BubbleSort)—优化


主要代码实现:

//冒泡排序-对顺序表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);
        }
    }
    
}


//冒泡排序-对顺序表L作冒泡排序(正宗冒泡排序算法)
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);
        }
    }
}

//冒泡排序-对顺序表L冒泡排序进行优化
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;
            }
        }
    }
}

简单选择排序(SimpleSelectionSort)

简单排序算法(SimpleSelectonSort)就是通过n-i次关键词⽐较,从n-i+1个记录中找出关键字最⼩的记录,并和第i(1<=i<=n)个记录进⾏交换.



主要代码实现:

//7.选择排序--对顺序表L进行简单选择排序void SelectSort(SqList *L){    int i,j,min;
    for (i = 1; i < L->length; 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)
            swap(L, i, min);
    }
}


直接插⼊排序(StraightInsertionSort)

直接插⼊排序算法(StightInsertonSort)的基本操作是将⼀个记录插⼊到已经排好序的有序表中,从⽽得到⼀个新的,记录数增1的有序表;






第一次j循环:


第二次j循环:


第三次j循环:


第四次j循环:


第五次j循环


直接排序完:


主要代码实现:

//直接插入排序算法--对顺序表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--)
                    //记录后移
                    L->r[j+1]=L->r[j];
            
            //插入到正确位置 可以把temp改为L->r[0]
            L->r[j+1]=temp;
        }
    }
}

空间复杂度:O(1) 解读:在直接插⼊排序中只使⽤了i,j,temp这三个辅助元素,与问题规模⽆关,空间复杂度为O(1)时间复杂度:O(n2) 最好的情况:顺序序列排序,例如{2,3,4,5,6}. 此时⽐较次数(C_{min})和移动次数(M_{min})达到最⼩值。


希尔排序及原理(ShellSort)

在插⼊排序之前,将整个序列调整成基本有序.然后再对全体序列进⾏⼀次直接插⼊排序


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






初始化increment=Length/2=5; 也就意味着整个数组被分割成{8,3},{9,5},{1,4},{7,6},{2,0}在这个分割中,进⾏部分直接插⼊排序 那么此时3,5,6,0这些⼩元素就会被调整到前⾯


i=5 i层循环从increment+1到length;也就是从5到9;从第5个元素到第9个元素都是待插⼊排序元素; 这⾥和插⼊排序的区别所在就是.插⼊排序增减量都是1.就是与相邻的元素进⾏⽐较.但是在希尔排序⾥.是⼀组的元素才进⾏插⼊排序;⽽3与9是⼀组;1与7是⼀组;5与4是⼀组;8与6是⼀组;同⾊系的数据直接才能进⾏插⼊排序



i = 6次循环


i = 7 次循环


i = 8 次循环



实现步骤









主要代码实现:

//希尔排序-对顺序表L希尔排序
void shellSort(SqList *L){    int i,j;    int increment = L->length;    
    //0,9,1,5,8,3,7,4,6,2
    //① 当increment 为1时,表示希尔排序结束
    do{
        //② 增量序列
        increment = increment/3+1;
        //③ 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]的位置上;
                L->r[j+increment] = L->r[0];
            }
        }
    }while (increment > 1);
}


堆排序(HeapSort)

堆是具有下⾯性质的完全⼆叉树:每个结点的值都⼤于或等于其左右孩⼦结点的值,称为⼤顶堆;如图1;或者每个结点的值都⼩于等于其左右孩⼦的结点的值,称为⼩顶堆,如图2



如果按照层序遍历的⽅式给结点从1开始编号,则结点之间的满⾜如下关系


堆结构:


堆排序(HeapSort)原理探索

堆排序(HeapSort)就是利⽤堆(假设我们选择⼤顶堆)进⾏排序的算法.它的基本思想:

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

步骤①构造初始堆。将给定⽆序序列构造成⼀个⼤顶堆(⼀般升序采⽤⼤顶堆,降序采⽤⼩顶堆)。

A.给定⽆序序列结构如下:


B.从最后⼀个⾮叶⼦结点开始(叶结点⾃然不⽤调整,第⼀个⾮叶⼦结点2,也就是下⾯的6结点)从左往右,从下往上进⾏调整.


B.找到第⼆个⾮叶⼦结点4.从[4,9,8]中找到最⼤的,4与9进⾏交换堆结构:⼤顶堆.(这个位置上的结点值要⼤于左右孩⼦)找到[9,4,8]三个结点找到最⼤值,放置到这个位置


C.此时的交换导致了⼦根结点[4,5,6]结构混乱,继续调整.从[4,5,6]中找到最⼤的结点6.交换4与6;那么经过3次调整.你会发现我们将刚刚⽆序序列调整成⼀个⼤顶堆结构;


步骤②将堆顶元素与末尾元素进⾏交换,使末尾元素最⼤。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第⼆⼤元素。如此反复进⾏交换、重建、交换

A.将堆顶元素9和末尾元素4进⾏交换.此时9将不参与后续的堆排序


B.重新调整结构,使其继续满⾜堆定义从[4,6,8]中找到最⼤的,4与8进⾏交换.经过调整此时我们⼜得到了⼀个⼤顶堆


C.再将堆顶元素8与末尾元素5进⾏交换,得到第⼆⼤元素8.


后续过程,继续进⾏调整,交换,如此反复进⾏,最终使得整个序列有序


堆排序(HeapSort)思路:

  • 将⽆需序列构建成⼀个堆,根据升序降序需求选择⼤顶堆或⼩顶堆
  • 将堆顶元素与末尾元素交换,将最⼤元素”沉"到数组末端;
  • 重新调整结构,使其满⾜堆定义,然后继续交换堆顶元素与当前末尾元素,反复执⾏调整+交换步骤,直到整个序列有序;

主要代码实现:


//大顶堆调整函数;/*
 条件: 在L.r[s...m] 记录中除了下标s对应的关键字L.r[s]不符合大顶堆定义,其他均满足;
 结果: 调整L.r[s]的关键字,使得L->r[s...m]这个范围内符合大顶堆定义.
 */
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];
        //⑥ 将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);
    }
}


堆排序(HeapSort)—⼤顶堆调整函数实现分析




堆排序(HeapSort)—复杂度分析

堆排序的时间复杂度为:O(nlogn)

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

堆排序的运⾏时间主要消耗在初始构建堆和重建对的反复筛选上;

初始化建堆过程时间:O(n) 推算过程: 

⾸先要理解怎么计算这个堆化过程所消耗的时间: 假设⾼度为k,则从倒数第⼆层右边的节点开始,这⼀层的节点都要执⾏⼦节点⽐较然后交换(如果顺序是对的就不⽤交 换);倒数第三层呢,则会选择其⼦节点进⾏⽐较和交换,如果没交换就可以不⽤再执⾏下去了。如果交换了,那么⼜要选择⼀⽀⼦树进⾏⽐较和交换。

总的时间计算为:s=2^(i-1) * (k-i);其中i表示第⼏层,2^(i-1)表示该层上有多少个元素,(k-i)表示⼦树上要⽐较的次数,如果在最差的条件下,就是⽐较次数后还要交换;因为这个是常数,所以提出来后可以忽略;S=2^(k-2)* 1+2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1) ===>因为叶⼦层不⽤交换,所以i从k-1开始到1; 这个等式求解,等式左右乘上2,然后和原来的等式相减,就变成了:S=2^(k-1)+2^(k-2)+2^(k-3).....+2-(k-1) 除最后⼀项外,就是⼀个等⽐数列了,直接⽤求和公式:S={ a1[1- (q^n)]} /(1-q); S=2^k-k-1;⼜因为k为完全⼆叉树的深度,所以(2^k)<= n<(2^k -1),总之可以认为:k=logn(实际计算得到应该是log(n+1)<k<=logn); 综上所述得到:S=n-longn-1,所以时间复杂度为:O(n)

更改堆元素后重建堆时间:O(nlogn) 推算过程: 1、循环 n-1次,每次都是从根节点往下循环查找,所以每⼀次时间是logn,总时间:logn(n-1)=nlogn -logn

堆排序的时间复杂度为:O(nlogn)

打印结果:


Demo点我