数据结构 学习笔记 排序

一、基本概念

1.1增排序和减排序

按关键字从大到小或从小到大划分。

1.2内部排序和外部排序

数据元素均在内存中即内部排序,否则则包含外部排序。

1.3稳定排序和不稳定排序

关键字相同的两个元素,排序后相对位置发生变化即不稳定,否则即稳定。

1.4排序算法的评价指标

时间复杂度   和   空间复杂度。

 

二、插入排序

2.1基本思想

将待排序表看作左右两个部分,左边为有序区,右边为无序区,整个排序过程就是将右边无序区的元素逐个插入到有序区中,以构成有序区。主要介绍 直接插入排序和希尔排序。

2.2直接插入排序

现直接给出代码和注释:

void insertSort(elementType A[n+1]) {
    for(int i = 2; i <= n; i++){        //I表示待插入元素的下标
        A[0] = A[I];                    //设置监视哨保存待插入元素,以腾出A[i]的空间
        j = I - 1;                      //j表示当前空位置的前一个
        while(A[j].key > A[0].key){     //搜索插入位置并腾出空位
            A[j+1] = A[j];
            j = j - 1;
        }
        A[j+1] = A[0];                  //插入元素
    }
}

算法分析:

1.稳定性:该算法为稳定算法。

2.空间性能:该算法仅需要一个记录的监视哨辅助空间。

3.时间性能:整个算法循环n-1次,每次循环中的基本操作为比较和移动元素,一般情况下为O(N^2).

 

2.3希尔排序(Shell Sort)

基本思想:将待排序的序列划分为若干组别,在每组内进行直接选择插入排序,以使得整个序列基本有序,然后再对整个序列进行直接插入排序。

这种排序的关键在于选组。而我们所决定的选择是将整个序列的长度的1/2在初始选择为步长。后面依次递减1/2。

伪代码:

void ShellSort(elementType A[n+1], int dh) { //dh means the ORIGINAL FOOTSTEP
    while(dh>=1) {
        for(I = dh + 1; I <= n; I++){
            temp = A[I];
            j = I;
            while(j > d && temp.key<A[j-dh].key){
                A[j] = A[j-dh];
                j = j - dh;
            }
            A[j] = temp;    

        }
            dh = dh/2;
    }


}

算法分析:

希尔排序是分组插入排序,先按照规定将元素分组,同一组内采用直接插入排序。

对比希尔排序和直接插入排序,希尔排序除了分组循环外,其余同插入排序几乎完全一致,只是步长从1变为了dh

1.该算法为不稳定算法。

2.空间复杂度为O(1)

3.时间复杂度为O(nlog2N)

   与分区方法有很大关系

   性能优于直接插入排序,时间复杂度介于O(n)和O(n^2)之间,大致为O(1.3)或O(1.5)

 

三、交换排序

两两比较待排序元素,发现倒序则交换。

3.1冒泡排序

逐个比较两相邻元素,发现倒序则交换。

典型做法是从后往前(从下往上)逐个比较相邻2个元素,发现倒序则交换。

每次扫描一定能将当前最小/大的元素交换到最终位置,如同水泡冒出水面

伪代码:

void bubbleSort(int A[]){
    for(int I = 1; I < n; I++){
        for(int j = n; j >= I + 1; j--){
            if(A[j].key<A[j-1].key){
                swap(A[j],A[j-1]; //SWAP in IOSTREAM
            }
        }
    }
}

改进的冒泡排序
接下来考虑一种极端情况:序列本身就是有序。

此种情况下,依然将进行O(n^2)级别的扫描。

很明显不够划算。

因此,我们可以设置一种含有标志是否已经交换完成的标志。这样作为每次冒泡排序完成后是否还需要继续的标志。

因此得到的改进算法如下:

void bubbleSort(int A[n+1]) {
I = 1;
do{
    exchanged = FALSE; // As a sign of Exchanged or not
    for(j = n; j >= I + 1; j--) {
        if(A[j].key < A[j-1].key){
            swap(A[j],A[j-1]);
            exchanged = TRUE;
            }
        }
    I++;
    }while(I<=n-1&&exchanged == TRUE);
}

算法分析:

稳定性:稳定排序

空间复杂度:O(1)的辅助空间

时间复杂度:

受到数据表初始状态影响大。

最好情况:正序 比较n-1次,交换 0 次, 时间复杂度O(n)

最坏情况:全部逆序 比较与交换 均为 n*(n-1)/2;

一般:O(n^2)

 

3.2快速排序

3.2.1基本思想:分治法。

选定一个元素作为中间元素,然后将表中所有元素与之比较:

比其小的放在表的前面;

比其大的放在表的后面;

该元素放在两部分中间做划分,这就是其最终位置。

这样就可以得到一个划分(二分)

然后对左右子表再分别进行划分。

快速排序通过一趟排序将排序序列分成左右两部分,使得左边任意元素均不大于/小于右边任意元素,并将中间元素放到最终位置。

3.2.2操作方法

选择第一个元素作为中间元素

1.先保存该元素到其他位置,腾出该位置。

2.从后往前扫描一个比中间数小的元素,并将其放置到(1)中的空位置上,此时后面空出一个位置。

3.从前往后扫描一个比中间数大的元素,并将其放置到(2)中的空位置上,此时前面空出一个位置。

重复2、3直到两边扫描到的空位重合,此时将中间元素放在空位中。

 

3.3.3算法设计

分区算法

1.保存中间元素的值到临时变量x以腾出空间,并且用low指向该元素,即x = A[low];

2.从后往前搜索比这个数字小的元素,并将其放在空位上,从而在后面腾出一个位置(high指向)

3.从前往后扫描到比这个数字大的元素,将其放置在(2)中的high上,从而使得前面空出一个位置(low指向)

重复2、3直到两边扫描的位置重合(low==high,即在该空位前没有更大的元素,此后没有更小的元素)因而可以将中间元素放在此位置,该元素归位。

void Partition(int A[], int low,int high, int &mid) {
    //low 分区的第一个元素下标,high 作为最后一个元素下标
    //mid为中间元素
    A[0] = A[low];
    while(low < high) {
        //A[high] >= mid元素则不交换,high左移
        while(low < high && A[high].key >= A[0].key) high--;
        //右区间遇到第一个小于mid的元素,移动到 A[low]
        //此时A[low]的元素已经取到A[0]
        //同时A[high]已经移动,其为空位置,可以存放其他数据
        A[low] = A[high];

        //A[low]<= mid 元素,则不交换,low右边移动
        while(low < high && A[low].key <= A[0].key) low++;

        //左区间遇到第一个大于此中间元素的值,移动到 A[high]
        //此时A[high]空
        A[high] = A[low];
    }
    //此时low == high 为目标的空位置
    A[low] = A[0];//将中间元素移动到目标位置
    mid = low;  //返回本次中间值的最终位置
}

快速排序即用到上述的分区算法

void QuickSort(int A[n], int low, int high){
    int mid; // mid 由Partition函数给出
    if(low <high){
        Partition(A,low,high,mid);
        QuickSort(A,low,mid-1);
        QuickSort(A,mid+1,high);
    }
}

算法分析:

1.稳定性:不稳定排序

2.空间复杂度:需要一个辅助空间

3.时间复杂度:

理想情况:每次选择元素正好两等份子表。整个算法复杂度为O(nlog2N)

最坏情况:每次选择的元素恰为最大/最小。即需要(n-1)次划分,扫描(n-i+1)次。整个复杂度为O(n^2)

一般情况:O(K*nlog2^N)

分析可得:划分中中间元素的选择非常重要,因此改进选择为:比较子表第一个、最后一个、中间元素。选取中值作为枢纽元素。

而快排目前也被认为是内部排序最优解之一。

 

四、选择排序

基本思想:在每次排序中选出关键字最小/最大的元素放在最终位置。

4.1简单(直接)选择排序

通过在待排序子表中完整的比较一遍以确定最值元素,并将该元素放在子表的最前/后面。

void SelectSort(int A[],int n) {
    //1~n
    for(int i = 1; i < n; i++) {
        int min = i;
        for(int j = i + 1; j < n; j++) {
            if(A[j] < A[min])
                min = j;
            if(min!=i) {
                swap(A[min],A[i]);
            }
        }
    }
}

算法分析:

稳定性:不稳定排序。

空间复杂度:需要一个额外空间。O(1)

时间复杂度:

共比较n*(n-1)/2次

最多交换n-1次,一趟最多交换1次

O(n^2)

 

4.2堆排序

4.2.1堆及其基本概念

堆实际上是一棵完全二叉树

​​​​​·若其每个结点均不大于其左右孩子的值,称为小根堆(根结点的值最小)

·若其每个结点均不小于其左右孩子的值,称为大根堆(根结点的值最大)

可见,若某序列为堆,其堆顶必为序列中的最大值或最小值。

 

堆排序的基本思想:

假设要求递增排序且已有一个大根堆

1.输出根

2.用二叉树的最后一个结点替代根,重新调整堆(待排序元素-1)

3.重复上述直到输出全部结点。

可见,要解决两个问题:

一是如何建立初始堆、二是输出根后如何调整堆。

 

4.2.2堆的筛选(调整)

1.输出根,用二叉树最后一个结点代替新的根。

2.调整堆,此时,除了跟结点和其左右孩子违反条件外,其余左右子树仍然满足条件。即整个序列不是堆,但其左右子树仍然是堆。

如何调整:

1.由于其左右子树是堆,此时左右孩子结点的值分别是两个子树中的最大值。因此,新的堆顶只可能从当前根点、其左右孩子中产生,故可以比较这三者得到。

2.如果当前根结点已经是最大值,即已经是堆,则无需调整;否则将左右孩子中的最大值与根对换。

但是调整之后可能违反子树中堆的大小,因此需要在执行调换的子树中继续进行。

 

算法设计:

1.保存临时根的值到一个变量(设为x)用i标记该结点。

2.比较i结点的左右孩子和x的最大值:

2.1     i结点没有左右孩子,即已经到达叶子结点。将x填到i结点中。

2.2     i结点的左右孩子的值小于x的值,表示搜索到了填充位置,将x填入i结点中。

2.3     否则将左右孩子中的最大填充在i结点中,从而出现新的空位,因此,同样用i指示,并且转2.2继续执行。

整理可得 所需参数:

调整中,堆顶的下标不一定为1,因此需要将堆顶的下标作为参数---K,输出根之后,参与运算的元素个数减一,因此,需要将当前序列的元素个数作为参数---M,加上数组参数A[].

void sift(int A[], int k, int m) {
    //调整以K为根的子树序列为堆
    //其中K为子树根,M为最大元素编号
    //假设以2K和2K+1为根的左右子树均为堆
    int x = A[k];   //临时保存当前根值,空出位置
    bool finished = false;//设置未结束标志
    int i = k;            //i指示空位,子树根
    int j = 2*i;          //j指向k的左孩子结点
    while(j<=m && !finished) {
        //确定i结点不是叶子且未搜索结束
        if(j < m && A[j] < A[j + 1])
            j = j +1;//找出i左右孩子中的最大者,用j指向
        if(x>=A[j])
            finished = true;
                    //根值最大,无需再调整,结束标志置真
        else {
            A[i] = A[j];    //最大值A[j]上升为树根
            i = j;          //跟新子树根i为j继续调整j以下的子树为堆
            j = 2 * j;      //继续下筛,i仍为子树树根,j指向其左孩子结点
        }
    }
    A[i] = x;               //循环结束i即为x的最终位置,使得K为根的子树为大根堆
}

 

从N/2开始从右往左、自下而上逐棵子树调整。

建立初堆:

for(int I = n/2; I>=1;i--){
    sift(A,i,n);
}

堆排序:

void HeapSort(int A[],int n){
    int i;
    //初建堆--由初始序列产生堆(此处为大根堆)
    //从第n/2结点开始往上筛,
    //直到1号结点(根、堆顶)
    for(i = n/2; i>=1;i--) {
        sift(A,i,n);
        //每次调用此函数,
        //都将以i为根结点的子树调整为堆。
    }//由堆序列产生排序序列,
    //此时整棵树(完全二叉树)为堆(此处为大根堆)
    for(i=n;i>=2;i--)
    {
        A[0]=A[i];  //完全二叉树最后一个结点保存到A[0],
        //空出位置i输出根A[1],即当前子树的根(堆顶) 
        A[i]=A[1];  //输出根,即A[1]保存到排序后的最终位置i
        A[1]=A[0];  //原第i元素暂作为“根”。
        //又A[1]=A[0]后可能破坏了当前树的堆属性,
        //需要从根结点1开始重新调整为堆
        //因为输出根,此时树的结点数为i-1。
        sift(A,1,i-1);
    }
}

算法分析:

稳定性:不稳定。

空间复杂度:需要一个辅助空间,O(1)

时间复杂度:

主要花费在建立初堆和调整堆上。

高度为h的堆,筛选算法中所进行的关键比较次数最多为2(h-1)次。

h=floor(log2n)+1;

即最多为log2N次

堆排序共调用筛选n-1次;建立初堆共调用筛选n/2次。

总复杂度为O(nlog2n)

 

五、归并排序

归并排序先设法将原序列划分为只含有1个元素的子表(视为有序)

然后反复选择两个有序子表进行合并直到合并后的序列长度为n

归并算法基于两个基本操作:划分和合并

划分操作将1个未排序序列划分成2个更短的子序列。

归并操作将2个或者多个有序子序列合并成1个更长的有序序列。

 

归并排序可以分为:

·自顶向下的

·自底向上的

归并排序同快速排序一样,都是分治法的典型应用。

5.1归并

(同线性表一样的三情况分情况讨论)

void merge(int A[],int B[],int C[],int la, int lb, int lc) {
    //非降序数组A,B前la,lb个元素合并到C 并且保持其次序
    int ia = 1, ib = 1, ic = 1;
    while(ia <= la && ib <= lb)
        if(A[ia]<=B[ib])
            C[ic++] = A[ia++];
        else 
            C[ic++] = B[ib++];
        while(ia <= la)
            C[ic++] = A[ia++];
        while(ib<=lb)
            C[ic++] = B[ib++];
}

算法分析:

对于A和B均是一遍扫描,整个时间复杂度为O(|A| + |B|)

 

而归并排序中归并的两个字序列要放在同一个表A中,因此要通过元素下标参数对两个字表进行定界。

通过三个参数low、mid、high来确定2个有序子序列。

第一个子序列放在A[low~mid]

第二个子序列放在A[mid+1~high]
此外,归并中要把归并后的元素放在一个临时表T中,T的大小与A相同归并完成后,再将T中的元素复制到A中。

Merge函数需要4个参数:

A[]存放元素序列 low序列第一个元素下标 high序列最后一个元素下标  mid划分点下标

改造后的归并序列:

void Merge(int A[], int low, int mid, int high) {
    int T[10005];
    int i, j, k;
    //i作为low~mid的下标  j作为mid+1~high的下标 k作为T的下标
    i = low;
    k = low;
    j = mid + 1;
    while(i<=mid && j<=high) {
        //A两个子表都有元素
        if(A[i] <= A[j]) {//A[i]较小
            T[k] = A[i];
            i++;
        }else {
            T[k] = A[j];
            j++;
        }
        k++;
    }
    //处理一个表结束,另一个尚未结束的场景
    while(i<=mid) {
        T[k] = A[i];
        i++;
        k++;
    }
    while(j<=high) {
        T[k] = A[j];
        j++;
        k++;
    }
    //复制回原表
    memcpy(A,T, sizeof(T));
}

自底向上

·将原序列视为(划分为)n个有序子序列,子序列长度为1,每个子表只有一个元素;

·当子序列长度小于N的时候,循环选择2个相邻有序子序列,归并

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值