【数据结构】第七章 排序

1.排序基本概念

1.拓扑排序是将有向图中所有结点排成一个线性序列,虽然也是在内存中进行的,但它不属于这里所提到的内部排序范畴,也不满足前面排序的定义。
2.对于任意序列进行基于比较的排序,求最少的比较次数应考虑最坏情况。对任意n个关键字排序的比较次数至少为 ⌈ l o g 2 ( n ! ) ⌉ \lceil log_2(n!) \rceil log2(n!)

对任意7个关键字进行基于比较的排序,至少要进行几次关健字之间的两两比较。
13次

2.插入排序

2.1 直接插入

  1. 算法思想:
  1. L ( i ) L (i) L(i) 放入哨兵中
  2. 前半部分有序表中,从后向前逐一与哨兵比较,查找 L ( i − 1 ) L(i-1) L(i1)有序表中第一个<= L ( i ) L(i) L(i)的元素位置 L ( k ) L (k) L(k).
  3. 比较过程中将大于哨兵的元素向后移动一位
  4. L ( i ) L (i) L(i) 复制到 L ( k + 1 ) L (k+1) L(k+1)
  1. 注:
  1. 算法从后向前寻找第一个小于等于自己的数即为停止条件
  2. L ( i ) L (i) L(i) 小于前半部分有序序列,则会与哨兵即自己比较,从而确定应当插入表头位置
  3. 算法边比较边移动元素
  1. 实现:
#include 


void insertsort(int a[],int n){
   int i,j;
   for(i=2;i<=n;i++){
      /* 加入判断可减少进入循环的次数 */
      if(a[i]<a[i-1]){
         a[0] = a[i];
         for (j = i-1;a[0]<a[j];j--){
            a[j+1] = a[j];
         }
         /* 此时j 指向位置<=a[0],为插入位置前一位置,所以需要+1*/
         a[j+1] = a[0];
      }
   }
}
void print_a(int a[],int n){
    for(int i = 1;i<=n;i++){
      printf("  %d  ",*(a+i));
   }
   printf("\n");
}

int main(){
   //指针地址为数组首地址
   int *a;                       
   int n;

   printf("please input n:\n");
   scanf("%d",&n);

   printf("please input number:\n");
   for(int i = 1;i<=n;i++){
      scanf("%d",(a+i));
   }
   print_a(a,n);
   insertsort(a,n);
   print_a(a,n);
     
   return 0;

}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 进行 n n n-1趟,比较次数与移动次数取决于表的初始状态
  3. 时间复杂度:
    1)最好 (有序): O ( n ) O (n) O(n) 。(只比较,不移动元素)
    2 ) 最坏(逆序): O ( n 2 ) O (n^2) O(n2) 。(比较次数 ∑ i = 2 n i \sum\limits_{i=2}^{n}i i=2ni, 移动次数 ∑ i = 2 n ( i + 1 ) \sum\limits_{i=2}^{n}(i+1) i=2n(i+1)
    3)平均: O ( n 2 ) O (n^2) O(n2)
  4. 稳定性:稳定

2.2 折半插入

  1. 算法思想:
  1. L ( i ) L (i) L(i) 放入哨兵中
  2. 通过折半查找的方法,确定 L ( i ) L (i) L(i)在有序表 L ( i − 1 ) L (i-1) L(i1)中的位置 L ( k ) L (k) L(k)
  3. 统一将 L ( k ) L (k) L(k)~ L ( i − 1 ) L (i-1) L(i1) 向后移动
  4. L ( i ) L (i) L(i) 复制到 L ( k ) L (k) L(k)
  1. 注:
  1. 折半法出现 h i g h < l o w highhigh<low 的情况即为: h i g h high high位置的值小于关键字, l o w low low位置的值大于关键字,所以, l o w low low的位置(即 h i g h + 1 high+1 high+1)为插入元素位置。
  2. 统一将 l o w low low~ L ( i − 1 ) L(i-1) L(i1) 向后移动一位。
  3. 与直接插入相比,仅减少了比较元素次数。
  4. 数据量小的排序表,折半插入往往能表现出很好的性能。
  1. 实现:
void br_insertsort(int a[],int n){
    int low ,high ,mid ;
    for(int i = 2;i<=n;i++){
        low = 1;
        high = i-1;
        a[0] = a[i];
        while(low<=high){
            mid = (low+high)/2;
            if(a[mid]<a[0]) {low = mid+1;}
            else high = mid-1;
        }
        for(int j=i-1;j>=low;j--){
            a[j+1] = a[j];
        }
        a[low] = a[0];
    }
}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 时间复杂度: O ( n 2 ) O (n^2) O(n2)
  3. 比较元素次数: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n) 与排序表初始状态无关,仅取决于元素个数 n n n
  4. 移动次数:(与直接插入相同) 依赖排序表的初始状态
  5. 稳定性:稳定

2.3 希尔排序(缩小增量排序)

  1. 算法思想:
  1. 取步长 d k 1 dk1 dk1 (一般为表长的一半 n / 2 n/2 n/2), 将所有距离为 d k 1 dk1 dk1的倍数的记录分为一组
  2. 对各组进行直接插入排序(此时,每个元素的前一比较位置为 i − d k 1 i-dk1 idk1
  3. 取步长 d k 2 dk2 dk2( d k 2 = d k 1 / 2 dk2 = dk1/2 dk2=dk1/2), 继续进行直接插入排序。直到 d k = 1 dk=1 dk=1,排序完成。
  1. 实现:
void shellsort(int a[], int n){
    for(int dk = n/2;dk>=1;dk = dk/2){
        int i = 1;
        int j;
        //直接插入排序思想
        for(i =i+dk;i<=n;i++){
            if(a[i]<a[i-dk]){
                a[0] = a[i];
                for(j = i-dk;j>0&&a[j]>a[0];j = j-dk){
                        a[j+dk] = a[j];
                    }
            a[j+dk] = a[0];
            }
        }
    }
}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 时间复杂度:最坏: O ( n 2 ) O (n^2) O(n2) (复杂度依赖于增量序列的函数)
  3. 稳定性:不稳定
  4. 仅适用于线性表为顺序存储的情况

对有n个元素的顺序表采用直接插入排序算法进行排序,在最坏情况下所需的比较次数是();在最好情况下所需的比较次数是()。
待排序表为反序时,直接插入排序需要进行 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 次比较;待排序表为正序时,只需进行 n − 1 n-1 n1 次比较。

3.交换排序

3.1 冒泡排序

  1. 算法思想:(升序排序,从后向前冒泡,先确定小元素位置)
  1. 从后向前,两两比较元素大小,若 L ( j ) < L ( j − 1 ) L(j) < L(j-1) L(j)<L(j1) 交换两元素,将 j j j 减1 继续比较 L ( j ) L(j) L(j) L ( j − 1 ) L(j-1) L(j1)
  2. 第一趟结束后,将最小元素放在最前,位置确定,下一趟不参与比较(外层循环结束判定条件 j > i j > i j>i, i i i 随趟数增加而增加),进行第二趟排序。
  3. 在循环中设置标志,每趟排序判断是否发生交换元素,若未发生则排序完成。
  1. 注:
  1. 每趟排序都会将一个元素放在最终位置
  2. 冒泡排序最多做 n − 1 n-1 n1
  1. 实现:
void bubble_sort(int a[],int n){
    int flag = 0,temp;
    for(int i=1;i<=n;i++){
        //此处判别条件为j>i,
        //每趟确定一个位置元素,所以不用重复比较
        for(int j=n;j>i;j--){
            if(a[j]<a[j-1]){
                temp = a[j];
                a[j] = a[j-1];
                a[j-1] = temp;
                flag = 1;
            }
        }
        //return 直接退出函数
        if(flag == 0) return ;
    }
}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 时间复杂度:
    1)最好 (有序): O ( n ) O (n) O(n) 。(比较 n − 1 n-1 n1 次,移动0次)
    2 ) 最坏(逆序): O ( n 2 ) O (n^2) O(n2) 。( n − 1 n-1 n1 趟排序)
    3)平均: O ( n 2 ) O (n^2) O(n2)
  3. 稳定性:稳定

3.2 快速排序

  1. 算法思想:
  1. 传入 a [ ] a[ ] a[],表的首位,末位。判断 l o w < h i g h ? low < high ? low<high?
  2. 选择表中第一个元素作为基准( a [ l o w ] a[ low ] a[low]),放入 a [ 0 ] a[ 0 ] a[0]
  3. a [ h i g h ] a[ high ] a[high]从后向前寻找第一个小于基准的值,放入 a [ l o w ] a[ low ] a[low],然后 a [ l o w ] a[ low ] a[low]从前向后寻找第一个大于基准的值,将更新后的 a [ h i g h ] a[ high ] a[high]放入,交替进行,直到 l o w = h i g h low = high low=high
  4. 返回基准插入位置,将表分为两个子表,进行下一趟排序。
  1. 注:
  1. 借助递归栈完成每趟排序
  2. 递归调用时,首先判断 l o w < h i g h ? low < high ? low<high?,此为递归跳出条件( l o w = h i g h low = high low=high时,说明表中只有一个元素,子表有序,跳出递归)
  3. 快速排序是对冒泡排序的一种改进,基于分治的思想
  4. 每趟确定子表基准元素位置。
  5. 性能主要取决于划分操作的好坏。
  6. 提高算法效率:
    1) 子序列规模小时,采用直接插入排序
    2) 选一个可将数据平分的基准
  7. 快速排序是所有内部排序算法中平均性能最优的
  1. 实现:
int partition(int a[], int low, int high){
    //首先将基准放入a[0]中
    //每次运行确定一个基准元素
    a[0] = a[low];
    while(low<high){
        //从后向前寻找high小于基准元素时,也需判断high与low的关系
        while(low<high&&a[high]>=a[0]){ high--;}
        a[low] = a[high];
        while(low<high&&a[low]<=a[0]) {low++;}
        a[high] = a[low];
    }
    a[low] = a[0];
    return low;
}

void quick_sort(int a[],int low, int high){
    if(low<high){//递归跳出条件
        int pos = partition(a,low,high);
        quick_sort(a,low,pos-1);
        quick_sort(a,pos+1,high);
    }
}
  1. 性能分析:
  1. 空间复杂度: 容量与递归调用的最大深度一直
    1)最好 : ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1)\rfloor log2(n+1)
    2 ) 最坏: O ( n ) O (n) O(n) 。( n − 1 n-1 n1 次递归)
    3)平均: O ( l o g 2 n ) O (log_2n) O(log2n)
  2. 时间复杂度:运行时间与划分是否对称有关
    1)最好 (平均划分): O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)
    2 ) 最坏(基本有序或逆序): O ( n 2 ) O (n^2) O(n2)
    3)平均: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)
  3. 稳定性:不稳定

4. 选择排序

选择排序:元素比较次数与序列的初始状态无关始终是 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2

4.1 简单选择排序

  1. 算法思想:(升序排序,先确定小元素位置)
  1. i i i趟排序,即从 L ( i ) L(i) L(i)~ L ( n ) L(n) L(n)中选出最小元素
  2. 设置 m i n min min,判断 L ( i ) L(i) L(i)是否等于 L ( m i n ) L(min) L(min), 若 L ( m i n ) < L ( i ) L(min) < L(i) L(min)<L(i),交换两元素值
  3. 继续选择 L ( i + 1 ) L(i+1) L(i+1) ~ L ( n ) L(n) L(n)中最小元素,放在 L ( i + 1 ) L(i+1) L(i+1)
  1. 注:
  1. 每趟排序都会将一个元素放在最终位置
  2. 经过 n − 1 n-1 n1 趟排序就可使得排序表有序
  1. 实现:
void sim_selesort(int a[], int n){
    for(int i=1;i<=n;i++){
        int min = i,temp;
        for(int j=n;j>i;j--){
            if(a[min]>a[j]){
                min = j;
            }
        }
        if(min!=i){
            temp = a[i];
            a[i] = a[min];
            a[min] = temp;
        }
    }
}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 时间复杂度: O ( n 2 ) O (n^2) O(n2) (元素比较次数与序列的初始状态无关始终是 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2
  3. 稳定性:不稳定

4.2 堆排序

  1. 算法思想:(大根堆)
  1. 设计建堆函数(从 n / 2 n/2 n/2开始,递减,使双亲大于孩子结点),设计向下调整函数,判断双亲结点与子孙结点大小(从当前双亲结点判断到表尾,每次步长 i ∗ 2 i*2 i2,将子树中最大值放在双亲结点)
  2. 排序过程:先建堆,然后交换首尾元素(将最大值放在表尾),利用向下调整函数将 1 1 1~ n − 1 n - 1 n1中最大值放在表首,交换首尾元素
  3. 排序过程,堆大小每次减一,最大值放在最后
  1. 注:
  1. 堆常被用来实现优先级队列
  2. 删除堆顶元素时,堆顶元素与最后一个元素互换,再从根结点自上向下调整
  3. 插入元素时,将新节点放在堆尾,对这个新结点执行向上调整操作
  1. 实现:
#include
//将第一元素调整为最大元素函数
//1.获得参数为,数组,双亲结点位置,表长
//2.将双亲结点给a[0],以便将来交换双亲与孩子结点值
//3.向下循环寻找大于双亲结点的孩子结点,每次步长为:双亲结点*2
//4.若碰到 双亲结点 > 孩子结点  则退出循环
//  原因1.建堆时,函数是从最后一个双亲结点开始判断,建的初始堆满足双亲结点大于孩子结点
//      2. heapsort函数中调用时,初始堆满足上大下小,不用担心有孙子结点大于孩子结点的情况


void adjustdown(int a[],int k,int n){
    a[0] = a[k];
    for(int i=2*k;i<=n;i=i*2){
        if(i<n&&a[i]<a[i+1]){i++;}
        if(a[0]>=a[i]) break;
        else {
            a[k] = a[i];    //  孩子结点的值给双亲
            k = i;          //k 指向孩子结点,方便赋值
        }
    }
    a[k] = a[0];        //找到k 的最终位置后,将a[0]的值给a[k]
}
//建立大顶堆,从n/2开始向前比较双亲结点,使第一个元素为最大元素
void buildmaxheap(int a[],int n){
    for(int i = n/2;i>0;i--){
        adjustdown(a,i,n);
    }
}
//1.先建立大顶堆(使第一个元素最大)
//2.交换第一个与最后一个元素
//3.在1~n-1个元素中找最大值(调用函数),继续交换首尾元素
//4.最终得到从小到大排序表
void heapsort(int a[],int n){
    buildmaxheap(a,n);
    for(int i=n;i>1;i--){
        int temp;
        temp = a[i];
        a[i] = a[1];
        a[1] = temp;
        adjustdown(a,1,i-1);//i = n;所以调整元素为1~i-1
    }
}

//向上调整操作(插入元素,插入堆尾)
void adjustup(int a[],int k){
    a[0] = a[k];
    for(int i = k/2;i>0;i=i/2){
        if(a[0]>a[i]){
            a[k] = a[i];
            k = i;
        }
    }
    a[k] = a[0];
}
  1. 性能分析:
  1. 空间复杂度: O ( 1 ) O (1) O(1)
  2. 时间复杂度(最好、最坏、平均): O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)(建堆时间 O ( n ) O( n ) O(n) n − 1 n-1 n1次向下调整操作,每次时间复杂度为 O ( h ) O(h) O(h)
  3. 稳定性:不稳定

向具有n个结点的堆中插入一个新元素的时间复杂度为(),删除一个元素的时间复杂度为().
在向有 n n n个元素的堆中插入一个新元素时,需要调用一个向上调整的算法,比较次数最多等于树的高度减1,由于树的高度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor+1 log2n+1,所以堆的向上调整算法的比较次数最多等于 ⌊ l o g 2 n ⌋ \lfloor log_2n \rfloor log2n
此处需要注意,调整堆和建初始堆的时间复杂度是不一样的

5.归并排序与基数排序

5.1 归并排序

归并排序与序列的初始状态无关

  1. 算法思想:(先递归后合并)
  1. 设计递归函数(找到两个最小合并表,逐层向上合并)。设计合并两表函数(有序表的合并)
  2. 有序表的合并:待排序两表相邻,传入两表总长的首末位置( l o w , h i g h low, high low,high),传入第二个表首位置( m i d = ( l o w + h i g h ) / 2 mid = (low+high)/2 mid=(low+high)/2),借助数组,判断值大小,完成合并。
  3. 递归函数中传入表的首末位置( l o w , h i g h low, high low,high),调用函数首先判断( l o w < h i g h low < high low<high递归退出条件),然后将表分为两份,递归调用两子表,整个算法思想:从子表开始两两合并,最终得到排序表。
  1. 注:
  1. 递归形式的2路归并排序基于分治思想
  2. 归并排序与序列的初始状态无关
  1. 实现:
#include
//实现两表的归并
void merge(int a[],int low, int mid, int high,int b[]){
    int t= 0;
    int i = low,j = mid+1;
    while(i<=mid&&j<=high){
        if(a[i]<=a[j]){ b[t++] = a[i++];}
        else {b[t++] = a[j++];}
    }
    while(i<=mid){b[t++] = a[i++];}
    while(j<=high){b[t++] = a[j++];}
    for(int k=0;k<t;k++){ a[low+k] = b[k];}

}
//递归调用函数归并两表
void merge_sort(int a[],int low ,int high,int b[]){
    if(low<high){//递归退出条件
        int mid = (low+high)/2; //划分两个子表
        merge_sort(a,low,mid,b);//对第一个子表进行归并排序
        merge_sort(a,mid+1,high,b);
        merge(a,low,mid,high,b) ;
    }
}

void print_a(int a[], int n){
    for(int i = 0;i<n;i++){
        printf("  %d  ",*(a+i));
    }
    printf("\n");
}

int main(){

    int a[5] = {3,5,4,1,2};
    int b[5];

    print_a(a,5);
    merge_sort(a,0,4,b);
    print_a(a,5);
    return 0;
}
  1. 性能分析:
  1. 空间复杂度: O ( n ) O (n) O(n) m e r g e ( ) merge() merge()中占 n n n个辅助空间)
  2. 时间复杂度: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)(每趟归并 O ( n ) O(n) O(n),共进行 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil log2n 趟归并)
  3. 稳定性:稳定

对于 N N N个元素进行 k k k路归并排序时,排序的趟数 m m m满足 k m = N k^m=N km=N,从而 m = l o g k N m=log_kN m=logkN,又考虑到 m m m为整数,所以 m = ⌈ l o g k N ⌉ m=\lceil log_kN\rceil m=logkN.这和前面的2路归并是一致的。

5.2 基数排序

  1. 算法思想:
    不基于比较进行排序,采用多关键字排序思想,借助“分配”和“收集“两种操作对单逻辑关键词进行排序(基于关键字各位的大小进行排序)。基数排序又分为最高优先级(MSD)与最低优先级排序(LSD)。
  2. 注:

基数排序不能对 f l o a t , d o u b l e float, double float,double类型的实数进行排序

  1. 性能分析:
  1. 空间复杂度: O ( r ) O (r) O(r) (一趟排序需要辅助空间为 r r r个队列)
  2. 时间复杂度: O ( d ( n + r ) ) O (d(n+r)) O(d(n+r)) 与序列的初始状态无关(需要进行 d d d趟分配与收集,一趟分配需要 O ( n ) O (n) O(n),一趟收集需要 O ( r ) O(r) O(r)
  3. 稳定性:稳定

将两个各有N个元素的有序表合并成一个有序表,最少的比较次数是(),最多的比较次数是()。
N N N 2 N − 1 2N-1 2N1
当一个表中的最小元素比另一个表中的最大元素还大时,比较的次数是最少的,仅比较 N N N次;而当两个表中的元素依次间隔地比较时,即 a 1 < b 1 < a 2 < b 2 < … < a n < b a1a1<b1<a2<b2<<an<b。时,比较的次数是最多的,为 2 N − 1 2N-1 2N1次。

6. 各种内部排序算法的比较

【数据结构】第七章 排序_第1张图片
注:

  1. 快速排序被认为是目前基于比较的内部排序法中最好的方法。
  2. 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
  3. 当文件的 n n n个关键字随机分布时,任何借助于“比较”的排序算法,至少需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的时间。
  4. n n n较小 ( n < = 50 ) (n<=50) n<=50,则可采用直接插入排序或简单选择排序。
  5. n n n较大,则应采用时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
  6. n n n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。

排序趟数与序列的原始状态无关的排序方法是()。
I.直接插入排序 Ⅱ.简单选择排序 Ⅲ.冒泡排序 IV.基数排序

交换类的排序,其趟数和原始序列状态有关,故冒泡排序与初始序列有关。直接插入排序:每趟排序都插入一个元素,所以排序趟数固定为 n − 1 n-1 n1;简单选择排序:每趟排序都选出一个最小(或最大)的元素,所以排序趟数固定为 n − 1 n-1 n1;基数排序:每趟排序都要进行“分配”和“收集”,排序趟数固定为 d d d

你可能感兴趣的:(笔记)