9. 数据结构--内排序

内排序

首先学这一章的时候发现,标题是内排序 但是之后也没学外排序,那么内排序和外排序到底是啥意思类?知识盲区了哈哈哈,以下是百度的结果:
内排序:指在排序期间数据对象所有存放在内存的排序。 外排序:指在排序期间所有对象太多,不能同一时候存放在内存中,必须依据排序过程的要求,不断在内,外存间移动的排序。

大纲

  • 排序的基本概念
  • 直接插入排序
  • 冒泡排序(bubble sort)
  • 简单选择排序
  • 希尔排序(shell sort)
  • 快速排序
  • 堆排序
  • 二路归并排序(merge sort)
  • 基数排序
  • 各种内排序算法的比较
  • 内排序算法的应用

排序的基本概念

给定一组记录r1,r2,…rn,其排序码分别为 k 1 , k 2 , … , k n k_1,k_2, …,k_n k1,k2,kn,将这些记录排成顺序为 r s 1 , r s 2 , … , r s n r_{s1},r_{s2},…,r_{sn} rs1,rs2,rsn的一个序列S,满足条件 k s 1 ≤ k s 2 ≤ k s n k_{s1}≤k_{s2}≤k_{sn} ks1ks2ksn

  • 排序算法的稳定性:如果在对象序列中有两个对象r[i]和r[j],它们的关键码 k[i] == k[j],且在排序之前,对象r[i]排在r[j]前面。如果在排序之后,对象r[i]仍在对象r[j]的前面,则称这个排序方法是稳定的,否则称这个排序方法是不稳定的
  • 性能分类
    • 简单但相对较慢的排序算法, O ( n 2 ) O(n^2) O(n2)
    • 先进的排序算法, O ( n l o g n ) O(n logn) O(nlogn)
    • 一些特殊的方法, O ( n ) O(n) O(n)
  • 排序分类
    • 交换类排序:冒泡排序、快速排序
    • 选择类排序:选择排序、堆排序
    • 插入类排序:插入排序、希尔排序
    • 归并类排序:归并排序
    • 分配式排序:分配排序、基数排序
    • 其他排序:图拓扑排序

  • 分析排序算法时,需测量的代价:
    1. 关键码比较的次数
    2. 记录交换的次数

一、三种代价为 O ( n 2 ) O(n^2) O(n2)的排序方法

1. 直接插入排序

1.1 算法基本思想和过程
1. 基本思想

逐个处理待排序的记录。每个新记录与前面已排序的子序列进行比较,将它插入到子序列中正确的位置。

2. 算法过程

1.在A[1…i-1]中查找A[i]的插入位置,A[1…j].key A[i].key < A[j+1…i-1].key;
2.将A[j+1…i-1]中的所有记录均后移一个位置;
3.将A[i] 插入(复制)到A[j+1]的位置上。

1.2 算法示例

在这里插入图片描述

插入排序

1.3 算法伪代码

定义了一个swap函数,用于交换数组中两个位置的元素

template <typename E, typename Comp>
void inssort(E A[], int n) {
    for (int i=1; i<n; i++)
        for (int j=i; (j>0)&&(Comp::prior(A[j], A[j-1])); j--)
            swap(A,j,j-1);
}

第二个循环在干什么?

1.4 算法分析

问题:

  1. 最好的情况,算法的比较次数和移动次数
    比较次数: ∑ i = 2 n = n − 1 \sum_{i=2}^n = n-1 i=2n=n1(从2开始 第一项无需比较)
    移动次数:0

  2. 最坏的情况,算法的比较次数和移动次数
    首先思考最坏情况是什么情况?
    关键字在记录序列中逆序有序
    比较次数:对于第i个元素比较i-1次,所以一共比较 ∑ i = 1 n − 1 i = 0 + 1 + 2 + . . . + n − 1 = n ( n − 1 ) 2 \sum_{i=1}^{n-1} i=0+1+2+...+n-1=\frac{n(n-1)}{2} i=1n1i=0+1+2+...+n1=2n(n1)
    移动次数:对于每次的交换操作,代码上如下; 因此每次交换要*3,第二个元素交换1次,第三个元素交换两次…
    总 比 较 次 数 = 3 ∗ ∑ i = 1 n − 1 i = 3 ∗ n ( n − 1 ) 2 总比较次数=3*\sum_{i=1}^{n-1} i=\frac{3*n(n-1)}{2} =3i=1n1i=23n(n1)

     TMP = A[j]; 
     A[j] = A[j-1];
     A[j-1] = TMP;
    
1.5 特点
  1. 算法简单
  2. 时间复杂度为 O ( n 2 ) O(n^2 ) O(n2),空间复杂度为 θ ( 1 ) \theta (1) θ(1)
  3. 初始序列基本(正向)有序时,时间复杂度为 θ ( n ) \theta (n) θ(n)
  4. 稳定排序

2. 冒泡排序

2.1 算法基本思想和过程
1. 基本思想

比较并交换相邻元素对, 直到所有元 素都被放到正确的地方为止

2. 算法过程

输入一个记录数组A,存放着n条记录

湖大的冒泡与众不同(尴尬):

每次都从数组的底部(存放最后一个记录)开始

  • 将第n-1个位置的值与第n-2个位置的值比较,若为逆序,则交换两个位置的值,
  • 然后,比较第n-2个位置和第n-3个位置的值,
  • 依次处理, 直至第2个位置和第1个位置值的比较(交换 )
  • 重复这个过程n-1次,整个数组中的元素将按关键码非递减有序排列。
2.2 算法示例

冒泡排序

2.3 算法伪代码
template < typename E, typename Comp >
void bubsort(E A[], int n) {
    for (int i=0; i<n-1; i++)
        for (int j=n-1; j>i; j--)
            if (Comp::prior(A[j], A[j-1]))
                swap(A, j, j-1);
}
// 改进的冒泡排序算法
template < typename E, typename Comp >
void bubsort(E A[], int n) {
    int flag;
    for (int i=0; i<n-1; i++)
    {
        flag=FALSE:
        for (int j=n-1; j>i; j--)
           if (Comp::prior(A[j], A[j-1]))
               { swap(A, j, j-1);flag=TRUE;}
           if(flag==FALSE) return;
     }
}

问题:改进点在哪?

2.4 算法分析

问题:

  1. 最好的情况,算法的比较次数和移动次数
    比较次数: ∑ i = 2 n = n − 1 \sum_{i=2}^n = n-1 i=2n=n1(从2开始 第一项无需比较)
    移动次数:0

  2. 最坏的情况,算法的比较次数和移动次数
    首先思考最坏情况是什么情况?
    关键字在记录序列中逆序有序
    比较次数:第i趟比较n-i次,一共需要n-1趟,排到对于第i个元素比较i-1次,所以一共比较 ∑ i = 1 n − 2 n − i = n − 1 + n − 2 + . . . + 1 = n ( n − 1 ) 2 \sum_{i=1}^{n-2} n-i=n-1+n-2+...+1=\frac{n(n-1)}{2} i=1n2ni=n1+n2+...+1=2n(n1)
    移动次数:由于冒泡也是基于交换的排序,因此,和直接插入一样
    总 比 较 次 数 = 3 ∗ ∑ i = 1 n − 1 i = 3 ∗ n ( n − 1 ) 2 总比较次数=3*\sum_{i=1}^{n-1} i=\frac{3*n(n-1)}{2} =3i=1n1i=23n(n1)

2.5 特点
  • 没有什么特殊的价值
    一种相对较慢的排序、没有插入排序易懂,
    而且没有较好的最佳情况执行时间
  • 冒泡排序为后面将要讨论的一种更好的
    排序提供了基础

3. 选择排序

3.1 算法基本思想和过程
1. 基本思想

第i次时“选择”序列中第i小的记录,并把该记录放到序列的第i个位置上。

2. 算法过程

输入一个记录数组A,存放着n条记录

  • 对于n个记录的数组,共进行n-1趟排序。
  • 每一趟在n-i+1个(i=1, 2, …, n-1)记录中通过n-i次关键字的比较选出关键码最小的记录和第
    i个记录进行交换
  • 经过n-1趟,整个数组中的元素将按关键码非递减有序排列。
3.2 算法示例

选择排序

3.3 算法伪代码
template < typename E, typename Comp >
void selsort(E A[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int lowindex = i; // Remember its index
        for (int j = n - 1; j > i; j--) // Find least
            if (Comp::prior(A[j], A[lowindex]))
                lowindex = j; // Put it in place
        swap(A, i, lowindex);
    }
}
3.4 算法分析

最好最坏,比较次数都相同,对n个记录进行简单选择排序,所需进行的关键字间的比较次数总计为
∑ i = 1 n − 1 ( n − i ) = n ∗ ( n − 1 ) 2 \sum_{i=1}^{n-1}(n-i) = \frac{n*(n-1)}{2} i=1n1(ni)=2n(n1)
移动的次数为 3 ∗ ( n − 1 ) 3*(n-1) 3(n1)

3.5 特点
  • 实质是延迟交换的冒泡排序

  • 不稳定的排序方法

  • 对于处理那些作一次交换花费代价(时 间)较多的问题,选择排序是很有效的, 适用于地址排序

4. 总结

对比三种N2算法

二、希尔排序

动机: 插入排序算法简单,在n值较小时,效率比较高; 在n值很大时,若序列按关键码基本有序,效率依然较高,其时间效率可提高到O(n)。

1. 算法基本思想和过程

1.1 基本思想

基本思想: 先将整个待排记录序列分割成若干个较小的子序 列,对子序列分别进行插入排序,然后把有序子序列组合起来;待整个序列中的记录“基本有序 ”时, 再对全体记录进行一次插入排序

1.2 算法过程

希尔排序过程: 输入一个记录数组A,存放着n条记录,以及一个(递 减)增量序列数组按照递减的次序(例如 8 4 2 1)

  • 对于每一个增量, 从数组的位置1开始,根据增量计算出子序列的最后 一个值的位置,然后调用基于增量的插入排序函数
  • 从数组的位置2开始,根据增量计算出子序列的最后 一个值的位置,然后调用基于增量的插入排序函数;
  • 依次类推,计算出当前增量下的所有子序列,并排序 依法处理下一个增量,直至增量为1,执行一次标准的简单插入排序

基于增量的插入排序:

输入一个记录数组A,起始位置i,结束位置n,增量incr

  • 先将数组中第i位置的记录看成是一个有序的子序列, 然后从第i+jincr位置的记录开始,依次对逐个增量位置进 行处理(插入)
  • 将第 i + j ∗ i n c r i+j *incr i+jincr 位置的记录X,依次与前面的第 i + ( j − 1 ) ∗ i n c r i+(j-1)*incr i+(j1)incr位置、第i+(j-2)*incr位置,…,第i位置的记录进行比较, 每次比较时,如果X的值小,则交换,直至遇到一个小 于或等于X的关键码,或者记录X已经被交换到第i位置, 本次插入才完成。
  • 继续处理,直至最后一个记录插入完毕,整个子序列有序

2. 算法示例

希尔排序

3. 算法伪代码

template < typename E, typename Comp >
void inssort2(E A[], int n, int incr) {
    for (int i = incr; i < n; i += incr)
        for (int j = i;
                (j >= incr) &&
                (Comp::prior(A[j], A[j - incr])); j -= incr)
            swap(A, j, j - incr);
}
template < typename E, typename Comp >
void shellsort(E A[], int n) { // Shellsort
    for (int i = n / 2; i > 2; i /= 2) // For each incr
        for (int j = 0; j < i; j++) // Sort sublists
            inssort2<E, Comp>(&A[j], n - j, i);
    inssort2<E, Comp>(A, n, 1);
}

4. 算法分析

分析Shell排序是很困难的,因此我们必须不加证明地承认Shell排序的平均运行时间是 Θ ( n 1 . 5 ) Θ(n^1.5) Θ(n1.5)(对于选 择“增量每次除以3”递减)。选取其他增量序列 可以减少这个上界。因此,Shell排序确实比插入 排序或任何一种运行时间为 θ ( n 2 ) \theta (n^2) θ(n2)的排序算法要快 有人在大量实验的基础上推出,当n在某个特定范围 内,所需比较和移动次数约为 n 1 . 3 n^1.3 n1.3 n → ∞ n \rightarrow \infty n 时,可减少到$n(log_2n)^2 $

5.特点

  1. 时间复杂度,取决于增量序列的选择,选择的好,效率优于插入排序

  2. 不稳定排序方法

  3. 适用于 n 较大情况(中等大小规模)

三、快速排序

动机: 分治思想:划分交换排序

1. 算法基本思想和过程

1.1 基本思想

在待排序记录中选取一个记录R(称为轴值pivot), 通过一趟排序将其余待排记录分割(划分)成独立 的两部分,比R小的记录放在R之前,比R大的记录 放在R之后,然后分别对R前后两部分记录继续进行 同样的划分交换排序,直至待排序序列长度等于1, 这时整个序列有序

1.2 算法过程

快速排序过程:快速排序(划分过程)用递归实现。

  • 若当前(未排序)序列的长度不大于1 返回当前序列
  • 否则 在待排序记录中选取一个记录做为轴值,通过划分算法将其余待排记录划分成两部分,比R小的记录放 在R之前,比R大的记录放在R之后;
  • 分别用快速排序对前后两个子序列进行排序(注意轴值已经在最终排序好的数组中的位置。无须继续处理)

选取轴值,划分序列的过程: 记录数组A,待排子序列左、右两端的下标i和j

选取待排序子序列中间位置的记录为轴值

交换轴值和位置j的值 依据在位置j的轴值,

将数组i-1到j之间的待排序记录划分为两个部分(i到k-1之间的记录比轴值小,k到j-1之间的 记录比轴值大) 从数组i-1到j之间的待排序序列两端向中间移动下标, 必要时交换记录,直到两端的下标相遇为止(相遇的位 置记为k) 交换轴值和位置k的值

PS :基准数的选择具体看题目

2. 算法示例

选取第一个数为基准数为例:

先从右往左找一个小于pivot的数,再从左往右找一个大于pivot的数,然后交换他们。

这里可以用两个变量i 和 j,分别指向序列最左边和最右边。

刚开始的时候让i指向序列的最左边,指向0位置。让j指向序列的最右边,指向n-1。

首先j开始移动,找到第一个小于pivot的位置,然后i向右移动,找到第一个大于pivot的位置

快速排序1

以选取中间位置为pivot为例

快速排序mid

描述一下第一趟的过程吧!

3. 算法伪代码

template < typename E, typename Comp >
void qsort(E A[], int i, int j) {
    if (j <= i) return; // List too small
    int pivotindex = findpivot(A, i, j);
    swap(A, pivotindex, j); // Put pivot at end
    // k will be first position on right side
    int k = partition<E, Comp>(A, i - 1, j, A[j]);
    swap(A, k, j); // Put pivot in place
    qsort<E, Comp>(A, i, k - 1);
    qsort<E, Comp>(A, k + 1, j);
}
template < typename E>
inline int findpivot(E A[], int i, int j) {
    return (i + j) / 2;
}
template < typename E, typename Comp >
inline int partition(E A[], int l, int r, E &pivot) {
    do { // Move the bounds in until they meet
        while (Comp::prior(A[++l], pivot));//l first
        while ((l < r) && Comp::prior(pivot, A[--r]));
        swap(A, l, r); // Swap out-of-place values
    } while (l < r); // Stop when they cross
    return l; // Return first pos on right
}

4. 算法分析

时间开销:

T ( n ) = { D ( 1 ) n < = 1 D ( n ) + T ( I 1 ) + T ( I 2 ) n > 1 T(n)=\begin{cases} {D(1)}&&n <=1\\ {D(n)+T(I_1)+T(I_2)}&&n>1 \end{cases} T(n)={D(1)D(n)+T(I1)+T(I2)n<=1n>1

  • 找轴值:常数时间

  • 划分: θ ( s ) \theta(s) θ(s),s是数列的长度

  • 最差情况: θ ( n 2 ) \theta(n^2) θ(n2)

  • 最佳情况:每次将数列分割为等长的两部分 θ ( n l o g n ) \theta(nlogn) θ(nlogn)

    quickbest

  • 平均情况:

    T ( n ) = c n + 1 n ∑ k = 0 n − 1 [ T ( k ) + T ( n − 1 − k ) ] , T ( 0 ) = T ( 1 ) = c T(n)=cn+\frac{1}{n}\sum_{k=0}^{n-1}[T(k)+T(n-1-k)],T(0)=T(1)=c T(n)=cn+n1k=0n1[T(k)+T(n1k)],T(0)=T(1)=c

    θ ( n l o g n ) \theta(nlogn) θ(nlogn)

空间开销

  • 辅助栈空间 O ( l o g n ) O(logn) O(logn)

5. 特点

  • 冒泡排序的改进,划分交换排序 (快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。)
  • 轴值的取值影响性能
  • 时间复杂度为 θ ( n l o g n ) \theta(nlogn) θnlogn
  • 不稳定的排序方法 •
  • 实际,快速排序是最好的内排序方法

快速排序的优化:

  • 更好的轴值
  • 对于小的子数列采用更好的排序算法
  • 用栈来模拟递归调用

四、归并排序

动机: 分治思想

1. 算法基本思想和过程

1.1 基本思想

基本思想: 将两个或多个有序表归并成一个有序表

1.2 算法过程

2 路归并排序

  1. 设有n个待排记录,初始时将它们分为n 个长度为 1的有序子表;
  2. 两两归并相邻有序子表,得到若干个长 度2为的有序子表;
  3. 重复2. 直至得到一个长度为n的有序表

合并两个有序子序列的过程:记录数组A,起始位置left,结束位置right,中间点mid

  • 首先将两个子序列复制到辅助数组中,
  • 首先对辅助数组中两个子序列的第一条记录进行比较,并把较小的记录作为合并数组中的第一个记录,复制到原数组的第一个位置上
  • 继续使用这种方法,不断比较两个序列中未被处理的记
    录,并把结果较小的记录依次放到合并数组中,直到两
    个序列的全部记录处理完毕
  • 注意要检查两个子序列中的一个被处理完,另一个未处理
    完的情况。只需依次复制未处理完的记录即可。

2. 算法示例

3. 算法伪代码

List mergesort(List inlist) {
    if (inlist.length() <= 1)return inlist;
    List l1 = half of the items from inlist;
    List l2 = other half of items from inlist;
    return merge(mergesort(l1),
                 mergesort(l2));
}
template <class Elem, class Comp>
void mergesort(Elem A[], Elem temp[],
               int left, int right) {
    int mid = (left + right) / 2;
    if (left == right) return;
    mergesort<Elem, Comp>(A, temp, left, mid);
    mergesort<Elem, Comp>(A, temp, mid + 1, right);
    for (int i = left; i <= right; i++) // Copy
        temp[i] = A[i];
    int i1 = left; int i2 = mid + 1;
    for (int curr = left; curr <= right; curr++) {
        if (i1 == mid + 1) //  左半边用尽(取右半边的元素)
            A[curr] = temp[i2++];
        else if (i2 > right) //    右半边用尽(取左半边的元素)
            A[curr] = temp[i1++];
        else if (Comp::lt(temp[i1], temp[i2]))// 左半边的当前元素小于右半边的当前元素(取左半边的元素)
            A[curr] = temp[i1++];
        else A[curr] = temp[i2++];
    }
// 优化版本
        template < typename E, typename Comp >
    void mergesort(E A[], E temp[], int left, int right) {
        if ((right - left) <= THRESHOLD) {
            inssort<E, Comp>(&A[left], right - left + 1);
            return;
        }
        int i, j, k, mid = (left + right) / 2;
        if (left == right) return;
        mergesort<E, Comp>(A, temp, left, mid);
        mergesort<E, Comp>(A, temp, mid + 1, right);
        for (i = mid; i >= left; i--) temp[i] = A[i];
        for (j = 1; j <= right - mid; j++)
            temp[right - j + 1] = A[j + mid];
        for (i = left, j = right, k = left; k <= right; k++)
            if (temp[i] < temp[j]) A[k] = temp[i++];
            else A[k] = temp[j--];
    }
/* 优化在何处?
  规模较小使用插入排序
  */

4. 算法分析

设需排序元素的数目为n,递归的深度为$logn $(简单起见,设n是2的幂)

第一层递归是对一 个长度为n的数组排序,下一层是对两个长度为 n/2的子数组排序,…,最后一层对n个长度为1 的子数组排序。 时间复杂度:T(n)=O(nlog2n) 在最佳、平均、最差情况下,时间复杂度为 θ ( n l o g n ) \theta(n log n) θ(nlogn)

递归代码的空间复杂度并不能像时间复杂度那样累加。尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

5. 特点

  • 需要辅助空间:$ \theta(n)$
  • 整个归并需要 [log2n] 趟
  • 时间复杂度: θ ( n l o g 2 n ) \theta(n log2n) θ(nlog2n)
  • 稳定的排序方法

五、堆排序

选择排序:树形选择排序,最值堆

1. 算法基本思想和过程

1.1 基本思想

首先将数组转化为一个满足堆定义的序列。 然后将堆顶的最值取出,再将剩下的数排成堆,再 取堆顶数值,…,如此下去,直到堆为空,就可 得到一个有序序列

1.2 算法过程

还记得堆的移除堆顶操作吗?

建堆:将输入序列用数组存储,利用堆的构建函数将数组转化为一个满足堆定义的序列(如果是递增排序, 则构建最大值堆,反之,构建最小值堆)

然后将堆顶的最大元素取出,再将剩下的数排成堆,再 取堆顶数值,…,如此下去,直到堆为空。

每次应将堆顶的最大元素取出放到数组的最后。 假设n个元素存于数组中的0到n-1位置上。把堆顶元 素取出时,应该将它置于数组的第n-1个位置。这时 堆中元素的数目为n-1个。再按照堆的定义重新排列 堆,取出最大值并放入数组的第n-2个位置。到最后 结束时,就排出了一个由小到大排列的数组

4、一组待排序序列为(46,79,56,38,40,84),则利用堆排序的方法建立的初始堆为( )。

A. 79,46,56,38,40,80

B. 84,79,56,38,40,46

C. 84,79,56,46,40,38

D. 84,56,79,40,46,38

建堆代码

2. 算法示例

堆排序

堆排序example1

堆排序example2

3. 算法伪代码

之前都有,懒得翻了我 之前的博客呀

4. 算法分析

堆排序的时间代价:

  • 建堆: θ ( n ) \theta(n) θ(n)
  • n次取堆的最大元素: θ ( l o g n ) \theta(log n) θ(logn)
  • 堆排序的总时间代价: θ ( 2 n l o g n ) \theta(2 n log n) θ(2nlogn)

堆排序的空间代价: 常数辅助空间: θ ( 1 ) \theta(1) θ(1)

过程有些复杂,总之堆排序的时间复杂度为 θ ( n l o g n ) \theta(nlogn) θ(nlogn)

5. 特点

• 基于堆数据结构,具有许多优点:

整棵树是平衡的,而且它的数组实现方式 对空间的利用率也很高,可以利用有效 的建堆函数一次性把所有值装入数组中。

堆排序的最佳、平均、最差执行时间均为 θ ( n l o g n ) \theta(nlogn) θ(nlogn),空间开销也是常数

堆排序是不稳定的排序算法

更适合于外排序,处理那些数据集太大而不适合在内存中排序的情况

六、 分配排序

桶排序和基数排序均属于分配排序。分配排序的基本思想:排序过程无须比较关键字,而是通过用额外的空间来"分配"和"收集"来实现排序,它们的时间复杂度可达到线性阶:O(n)。简言之就是:用空间换时间,所以性能与基于比较的排序才有数量级的提高!

此处例子参考

1. 基数排序

基数排序是对桶排序的一种改进,这种改进是让“桶排序”适合于更大的元素值集合的情况,而不是提高性能。

从最次的关键字开始排序,再从第二次的关键字排序,过程中参考第一次排序后元素间的相对顺序,以此类推直到最高关键字参考了次高关键的顺序而排序完成,排序结束。

比如字符串“abcd” “aesc” “dwsc” "rews"就可以把每个字符看成一个关键字。另外还有整数 425、321、235、432也可以每个位上的数字为一个关键字。

基数排序的思想就是将待排数据中的每组关键字依次进行桶分配。比如下面的待排序列:

             278、109、063、930、589、184、505、269、008、083

我们将每个数值的个位,十位,百位分成三个关键字: 278 -> k1(个位)=8 ,k2(十位)=7 ,k3=(百位)=2。

然后从最低位个位开始(从最次关键字开始),对所有数据的k1关键字进行桶分配(因为,每个数字都是 0-9的,因此桶大小为10),再依次输出桶中的数据得到下面的序列。

   930、063、083、184、505、278、008、109、589、269(从最次关键字开始排序)

再对上面的序列接着进行针对k2的桶分配,输出序列为:

   505、008、109、930、063、269、278、083、184、589(参考最次关键字来排序第二次关键字)

最后针对k3的桶分配,输出序列为:

   008、063、083、109、184、269、278、505、589、930(参考第二次关键字来排序最高关键字)

很明显,基数排序的性能比桶排序要略差。每一次关键字的桶分配都需要O(N)的时间复杂度,而且分配之后得到新的关键字序列又需要O(N)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2N) ,当然d要远远小于N,因此基本上还是线性级别的。但是,对比桶排序,基数排序每次需要的桶的数量并不多。而且基数排序几乎不需要任何“比较”操作,而桶排序在桶相对较少的情况下,桶内多个数据必须进行基于比较操作的排序。

待排序数组[62,14,59,88,16]简单点五个数字

分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样

| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号

将桶里的数字顺序取出来,

输出结果:[62,14,16,88,59]

再次入桶,不过这次以十位数的数字为准,进入相应的桶,变成下边这样:

由于前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,就是说,入完桶还是有序

| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |桶编号

因为没有大过100的数字,没有百位数,所以到这排序完毕,顺序取出即可

最后输出结果:[14,16,59,62,88]

对于n个数据的序列,假设基数为r,这个算法需要k趟 分配工作。每趟分配的时间为Θ(n+r),因此总的时间开 销为Θ(nk+rk)。因为r是基数,它一般是比较小的。可 以把它看成是一个常数。变量k与关键码长度有关,它是 以r为基数时关键码可能具有的最大位数。在一些应用中 我们可以认为k是有限的,因此也可以把它看成是常数。 在这种假设下,基数排序的最佳、平均、最差时间代价 都是Θ(n),这使得基数排序成为我们所讨论过的具有最 好渐近复杂性的排序算法

2. 桶排序

基本思想:设置若干个箱子,依次扫描待排序的记录 array[0],array[1],…,array[n - 1],把关键字等于 k 的记录全都装入到第 k 个箱子里(分配),然后按序号依次将各非空的箱子里的记录收集起来,从而完成排序。

举个例子:一年的全国高考考生人数为500万,分数使用标准分,最低100,最高900,没有小数,你把这500万元素(内容为分数)的数组排个序。

我们抓住了这么个非常特殊的条件,就能在毫秒级内完成这500万的排序,那就是:最低100,最高900,没有小数,那一共可出现的分数可能有多少种呢?一共有900-100+1=801,那么多种,想想看,有没有什么“投机取巧”的办法?方法就是创建801个“桶”,从头到尾遍历一次数组,对不同的分数给不同的“桶”加料.

比如有个考生考了500分,那么就给500分的那个桶(下标为500-100)加1,完成后遍历一下这个桶数组,按照桶值,填充原数组,100分的有1000人,于是从0填到999,都填1000,101分的有1200人,于是从1000到2019,都填入101.于是经过这次遍历之后所有记录都是有序的了。

很显然,如果分数不是从100到900的整数,而是从0到2亿,那就要分配2亿个桶了,这是不可能的,所以桶排序有其局限性,适合元素值集合并不大的情况。

当然桶里也可不止装一个元素,比如下图,在决定往那个桶插入的过程是用的关键字映射,但是实际插入具体桶时,可以用插入排序。

桶排序

结论

时间:

  1. 平均的时间性能

    时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
    快速排序、堆排序和归并排序

    时间复杂度为 O ( n 2 ) : O(n^2): O(n2):
    直接插入排序、冒泡排序和简单选择排序

    时间复杂度为 O ( n ) : O(n): O(n):
    基数排序

  2. 当待排记录序列按关键字顺序有序时

    直接插入排序和起泡排序能达到O(n)的时 间复杂度,

    快速排序的时间性能蜕化为O(n2)

  3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

空间:

  1. 所有的简单排序方法(包括:直接插入、 起泡和简单选择) 和堆排序的空间复杂度为O(1);
  2. 快速排序为O(logn),为递归程序执行过程中,栈 所需的辅助空间; 各种排序方法空间性能
  3. 归并排序所需辅助空间最多,其空间复杂度为 O(n);

稳定排序?

  1. 稳定的排序算法

    插入排序、冒泡排序和归并排序 分配排序和基数排序

  2. 不稳定的排序算法

    选择排序,堆排序 快速排序和shell排序

参考资料:

[1]https://blog.csdn.net/zcxwww/article/details/51439206

[2]杨晓波老师PPT

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是用C语言实现的代码: ```c #include <stdio.h> #include <stdlib.h> // 二叉树结点结构体 typedef struct TreeNode { int val; // 结点值 struct TreeNode* left; // 左子结点 struct TreeNode* right; // 右子结点 } TreeNode; // 插入结点 TreeNode* insert(TreeNode* root, int val) { if (root == NULL) { // 如果空树,则插入到根结点 TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode)); node->val = val; node->left = NULL; node->right = NULL; return node; } if (val < root->val) { // 如果插入的值小于当前结点的值,则插入到左子树 root->left = insert(root->left, val); } else { // 否则插入到右子树 root->right = insert(root->right, val); } return root; } // 计算树的高度 int height(TreeNode* root) { if (root == NULL) return 0; int leftHeight = height(root->left); int rightHeight = height(root->right); return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1; } // 打印二叉树 void printTree(TreeNode* root, int layer) { if (root == NULL) return; printTree(root->right, layer + 1); // 先打印右子树 for (int i = 0; i < layer - 1; i++) { printf(" "); } if (layer > 0) { printf(".--"); } printf("%d\n", root->val); printTree(root->left, layer + 1); // 再打印左子树 } int main() { TreeNode* root = NULL; // 根结点 int val; while (scanf("%d", &val) != EOF) { root = insert(root, val); // 插入结点 } int h = height(root); // 计算树的高度 printTree(root, h); // 打印二叉树 return 0; } ``` 输入样例: ``` 10 5 20 8 4 7 ``` 输出样例: ``` .---20 .--10 | `--8 | |--7 `--5 `--4 ``` 解释:该二叉树的结构如下: ``` 10 / \ 5 20 / \ 4 8 / 7 ``` 其中,叶子结点 4 和 7 的高度为 1,结点 5 和 8 的高度为 2,结点 10 的高度为 3,结点 20 的高度为 1。因此,树的高度为 3。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值