各种内排序汇总

八、排序

8.1 排序的基本概念

排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。

为了查找方便,通常希望计算机中的表是按关键字有序的。

确切定义:

  1. 输入n个记录 R 1 , R 2 , . . . , R n R_1,R_2,...,R_n R1,R2,...,Rn ,对应的关键字为 k 1 , k 2 , . . . , k n k_1,k_2,...,k_n k1,k2,...,kn
  2. 输出序列的一个重排 R 1 ′ , R 2 ′ , . . . , R n ′ R_1',R_2',...,R_n' R1,R2,...,Rn 使得, k 1 ′ ≤ k 2 ′ ≤ . . . ≤ k n ′ k_1'\leq k_2' \leq ... \leq k_n' k1k2...kn,其中 ≤ \leq 可以换成其他比较符号。

算法的稳定性。若待排序表中有两个元素 R i R_i Ri R j R_j Rj,其对应的关键字相同,即 k e y 1 = k e y 2 key_1 = key_2 key1=key2,且在排序前 R i R_i Ri R j R_j Rj 的前面,若使用某一排序算法排序后, R i R_i Ri 仍在 R j R_j Rj 前面,则称这个排序算法是稳定的。否则就是不稳定的。

需要注意,算法的稳定性并不能够衡量一个算法的优劣,主要是对算法的性质进行描述。

如果待排序表中的关键字不允许重复,则排序结果是唯一的,选择排序算法时的稳定性就无关紧要。

排序过程中,根据数据元素是否完全在内存中,可将算法分为:

  1. 内部排序,在排序期间全部元素存放在内存中的排序。
  2. 外部排序,指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断在内外存之间移动。

一般情况下,内排序算法在执行过程中都要进行两种操作:比较和移动。

通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。

当然,并非所有的内排序都需要基于比较排序,例如基数排序。

每种排序算法有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。

通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类。

内排序算法的性能取决于算法的时间复杂度和空间复杂度,时间复杂度一般是由比较和移动的次数比较的。

对于任意序列进行基于比较的排序,求最少的比较次数应考虑最坏情况。对任意 n 个关键字排序的比较次数至少为 ⌈ l o g 2 n ! ⌉ \lceil log_2n! \rceil log2n!

8.2 插入排序
8.2.1 直接插入排序

对于当前元素 L ( i ) L(i) L(i),进行的插入操作如下:

  1. 查找出 L ( i ) L(i) L(i) L [ 1... i − 1 ] L[1...i-1] L[1...i1] 中的插入位置 k。
  2. L [ k . . . i − 1 ] L[k...i-1] L[k...i1] 中的所有元素依次后移一个位置。
  3. L ( i ) L(i) L(i) 复制到 L ( k ) L(k) L(k)

通常可以将 L ( 2 ) − L ( n ) L(2) - L(n) L(2)L(n) 依次插入到前面已排好序的子序列中。

通常采用就地排序。空间复杂度为 O ( 1 ) O(1) O(1)

代码:

void InsertSort(ElemType A[], int n){
    int i, j;
    for(i = 2; i <= n; ++i){
        if(A[i] < A[n-1]){
            A[0] = A[i];
            for(j = i - 1;A[0] < A[j]; --j){
                A[j + 1] = A[j];
            }
            A[j + 1] = A[0];
        }
    }
}

最好情况下时间复杂度 O ( n ) O(n) O(n),最坏情况下时间复杂度为 O ( n 2 ) O(n^2) O(n2)

是一个稳定的算法。

适用于顺序存储和链式存储的线性表。

8.2.2 折半插入排序

即只改进查找元素待插入位置的过程,减少比较次数。

代码:

void InsertSort(ElemType A[], int n){
    int i, j, low, high, mid;
    for(i = 2;i <= n;++i){
        A[0] = A[i];
        
        low = 1;
        high = i - 1;
        while(low <= high){
            mid = (low + high) / 2;
            if(A[mid] > A[0]) high = mid - 1;
            else low = mid + 1;
        }
        
        for(j = i - 1;j >= high; --j){
            A[j + 1] = A[j];
        }
        A[high + 1] = A[0];
    }
}

比较次数减少的,约为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

而元素的移动次数并未发生改变,时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)

仍是一种稳定的排序算法。(upper_bound)

8.2.3 希尔排序

插入排序适合基本有序的排序表和数据量不大的排序表。

希尔排序又称缩小增量排序。

思想:

  1. 先将待排序表分割成若干形如 L [ i , i + d , i + 2 d , . . . , i + k d ] L[i,i+d,i+2d,...,i+kd] L[i,i+d,i+2d,...,i+kd] 的“特殊”子表,即把相隔某个“增量”的记录组成一个子表。
  2. 对各个子表进行直接插入排序。
  3. 当整个表中的元素已呈“基本有序”时,再对全体记录一次直接插入排序。

到目前位置,尚未求得一个最好的增量序列。

希尔提出的方法是, d 1 = n / 2 , d i + 1 = ⌊ d i / 2 ⌋ d_1 = n/2,d_{i+1} = \lfloor d_i/2 \rfloor d1=n/2,di+1=di/2,且最后一个增量为1。

代码:

void ShellSort(ElemType A[], int n){
    int dk, i;
    for(dk = n / 2;dk >= 1;dk = dk / 2){
        for(i = dk + 1;i <= n;++i){
            if(A[i] < A[i - dk]){
                // 部分直接插入排序
                A[0] = A[i];
                for(j = i - dk;j > 0 && A[0] < A[j];j -= dk){
                    A[j + dk] = A[j];
                }
                A[j + dk] = A[0];
            }
        }
    }
}

空间效率为 O ( 1 ) O(1) O(1)

时间复杂度分析较为困难,当 n 再某个特定范围时,希尔排序的时间复杂度约为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)

最坏情况下,希尔排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

稳定性:可能因划分到不同的子表而改变相对顺序,因此是不稳定的算法。

仅适用于线性表为顺序表的情况。

8.3 交换排序

交换指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。

8.3.1 冒泡排序

思想是:从后往前或从前往后两两比较相邻元素的值,若为逆序,则交换,知道序列比较完。结果是将最小的元素交换到待排序列的第一个位置或将最大的元素交换到待排序列的最后一个位置。

关键字最大或最小的元素如气泡一般逐渐向上“漂浮”。

下一趟冒泡时,前一趟确定的最小元素不再参加比较,这样最多 n − 1 n-1 n1 趟冒泡就能把所有元素排好序。

代码:

void BubbleSort(ElemType A[], int n){
    int i, j, flag;
    for(i = 0;i < n - 1;++i){
        flag = 0; // 记录是否发生交换 没有交换直接返回
        for(j = n - 1;j > i;--j){
            if(A[j - 1] > A[j]){
                swap(A[j - 1], A[j]);
                flag = 1;
            }
        }
        if(flag == 0) return;
    }
}

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

时间复杂度为 O ( n 2 ) O(n^2) O(n2),最好情况下为 O ( n ) O(n) O(n),平均情况下为 O ( n 2 ) O(n^2) O(n2)

冒泡排序也是一种稳定的排序算法。

双向冒泡排序。正反两个方向交替进行扫描。

即第一趟把最大的关键字放在最后,第二趟把最小的关键字放在最前。

代码:

void BubbleSort2(ElemType a[], int n){
    int low = 0, high = n - 1;
    bool flag = 1;
    while(low < high && flag){
        flag = 0;
        for(i = low;i < high;++i){
            if(a[i] > a[i + 1]){
                swap(a[i], a[i + 1]);
                flag = 1;
            }
        }
        high--;
        for(i = high;i > low;--i){
            if(a[i] < a[i - 1]){
                swap(a[i], a[i - 1]);
                flag = 1;
            }
        }
        low++;
    }
}

8.3.2 快速排序

基于分治算法。

  1. 在待排序表 L [ 1... n ] L[1...n] L[1...n] 中任意选取一个元素 p i v o t pivot pivot 作为枢轴(或基准),通常选取首元素,通过一趟排序将待排序表划分为两个独立的部分 L [ 1 , . . . , k − 1 ] L[1,...,k-1] L[1,...,k1] L [ k + 1 , . . . , n ] L[k+1,...,n] L[k+1,...,n],使得前面的元素都小于 p i v o t pivot pivot,后面的元素都大于等于 p i v o t pivot pivot。这个过程称为一趟快速排序,或一次划分。
  2. 分别递归地对两个子表重复该过程,知道每部分只有一个元素或为空为止。

在这里插入图片描述

代码:

int Partition(ElemType A[], int low, int high){
    ElemType pivot = A[low];
    // 划分方法
    while(low < high){
        while(low < high && A[high] >= pivot)
            --high;
        A[low] = A[high];
        while(low < high && A[low] <= pivot)
            ++low;
        A[high] = A[low];
    }
    A[low] = pivot;
    // ...
    return low;
}

void QuickSort(ElemType A[], int low, int high){
    if(low < high){
        int pivotpos = Partition(A, low, high);
        QuickSort(A, low, pivotpos - 1);
        QuickSort(A, pivotpos + 1, high);
    }
}

// 另一种 随机选取枢轴值的函数方式
int Partition2(ElemType A[], int low, int high){
    int rand_index = low + rand() % (high - low + 1);
    swap(A[rand_index], a[low]);
    ElemType pivot = A[low];
    // 外一种 更简便的划分方法
    int i = low, j;
    for(j = low + 1;j <= high;++j){
        if(A[j] < pivot) swap(A[++i], A[j]);
    }
    swap(A[i], A[low]);
    // ...
    return i;
}

空间效率:需要借助一个递归工作栈。容量与递归调用的最大深度一致。最好情况下 O ( l o g 2 n ) O(log_2n) O(log2n),最坏情况下,因为要进行 n − 1 n-1 n1 次递归调用,栈深度为 O ( n ) O(n) O(n)。平均情况下,栈深度为 O ( l o g 2 n ) O(log_2n) O(log2n)

时间效率:最坏情况下为 O ( n 2 ) O(n^2) O(n2)。理想情况下复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。快速排序是所有内部排序算法中平均性能最优的排序算法。

快速排序是一种不稳定的排序算法。

8.4 选择排序

基本思想是,每一趟在后面所有待排序的元素中选取关键字最小的元素,作为有序子序列的第 i i i 个元素,直到第 n - 1趟做完。

8.4.1 简单选择排序

代码:

// 简单选择排序
void SelectSort(ElemType A[], int n){
    for(i = 0;i < n - 1;++i){
        min = i;
        for(j = i + 1;j < n;++j)
            if(A[j] < A[min]) min = j;
        if(min != i)swap(A[i], A[min]);
    }
}

// 单链表上的选择排序
void SelectSort(LinkedList &L){
    LinkNode *h = L, *p, *q, *min, *qmin;
    L = NULL;
    while(h != NULL){
        p = min = h;
        q = qmin = NULL;
        while(p != NULL){
            if(p->data > min->data){
                min = p;
                qmin = q;
            }
            q = p;
            p = p->link;
        }
        if(min == h)h = h->link;
        else qmin->link = min->link;
        
        // 头插法 得到的是从大到小排序的结果
        min->link = L;
        L = min;
    }
}

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

时间复杂度始终是 O ( n 2 ) O(n^2) O(n2)

不是一种稳定的排序算法。

8.4.2 堆排序

首先将存放在 L [ 1... n ] L[1...n] L[1...n] 中的 n 个元素建成初始堆,由于堆(大顶堆)本身的特点,堆顶元素就是最大值。

输出堆顶元素之后,通常将堆底元素送入堆顶,向下调整使其保证大顶堆的性质。

再输出堆顶元素。如此重复,直到堆中元素仅剩一个为止。

建立大根堆的算法:

// 将元素 k 为根的子树进行调整
void HeadAdjust(ElemType A[], int k, int len){
    A[0] = A[k]; // A[0]起到一个暂存的作用
    for(i = 2 * k;i <= len;i *= 2){
        if(i < len && A[i] < A[i + 1]) ++i;
        if(A[0] >= A[i]) break;
        else {
            A[k] = A[i];
            k = i; // 继续向下走
        }
    }
    A[k] = A[0]; // 放到最终的位置上
}

void BuildMaxHeap(ElemType A[], int len){
    int i;
    for(i = len / 2;i > 0;--i){ // 从len / 2处开始调整
        HeadAdjust(A, i, len);
    }
}

建堆的时间复杂度为 O ( n ) O(n) O(n)。关键字的比较总次数不超过 4n。

递推式为 T ( n ) = 2 ∗ T ( n / 2 ) + O ( l g n ) T(n) = 2*T(n/2) + O(lg n) T(n)=2T(n/2)+O(lgn)

它的解是 T(n) = O(n)

在这里插入图片描述

堆排序算法:

void HeapSort(ElemType A[], int len){
    BuildMaxHeap(A, len);
    for(i = len;i > 1;--i){
        swap(A[i], A[1]);
        HeadAdjust(A, 1, i - 1);
    }
}

空间效率为 O ( 1 ) O(1) O(1)

时间效率为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

算法不稳定。

8.5 归并排序

归并的含义是将两个或两个以上的有序表组成一个新的有序表。

代码:

ElemType *B = (ElemType *) malloc((n + 1) * sizeof(ElemType));

void Merge(ElemType A[], int low, int mid, int high){
    int i, j, k;
    for(k = low;k <= high; ++k){
        B[k] = A[k];
    }
    for(i = low, j = mid + 1, k = i;i <= mid && j <= high; ++k){
        if(B[i] <= B[j]) A[k] = B[i++];
        else A[k] = B[j++];
    }
}

void MergeSort(ElemType A[], int low, int high){
    if(low < high){
        int mid = (low + high) / 2;
        MergeSort(A, low, mid);
        MergeSort(A, mid + 1, high);
        Merge(A, low, mid, high);
    }
}

空间效率:Merge() 操作中,辅助空间刚好为 n 个单元,所以算法的空间复杂度为 O ( n ) O(n) O(n)

时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

8.6 基数排序

不基于比较和移动进行排序,而基于关键字各位的大小进行排序。

是一种借助多关键字排序的思想中对单逻辑关键字进行排序的方法。

假设长度为 n 的线性表中每个结点 a j a_j aj 的关键字有 d d d 元组( k j d − 1 , k j d − 2 , . . . , k j 1 , k j 0 k_j^{d-1},k_j^{d-2},...,k_j^1,k_j^0 kjd1,kjd2,...,kj1,kj0)组成,满足 0 ≤ k j i ≤ r − 1   ( 0 ≤ j < n , 0 ≤ i ≤ d − 1 ) 0 \leq k_j^i \leq r-1\ (0\leq j<n,0\leq i\leq d-1) 0kjir1 (0j<n,0id1)。其中 k j d − 1 k_j^{d-1} kjd1 为最主位关键字, k j 0 k_j^0 kj0 为最次位关键字。

例如,每个关键字是 1000 以下的正整数,则基数 r = 10 r = 10 r=10,每个关键字由 3 为子关键字构成 K 1 K 2 K 3 K^1K^2K^3 K1K2K3,分别代表百位、十位和个位。

为实现多关键字排序,通常有两种方法:

  1. 最高位优先法,按关键字位权重递减依次逐层划分成若干更小的序列,最后将所有子序列依次连接成一个有序序列。
  2. 最低位优先法,按关键字权重递增依次进行排序,最后形成一个有序序列。

以 r 为基数的最低位优先基数排序,使用 r 个队列 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr1

过程:

  1. 分配:开始时,把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr1 各个队列置成空队列,然后依次考察线性表中的每个结点 a j ( j = 0 , 1 , . . . , n − 1 ) a_j(j = 0,1,...,n-1) aj(j=0,1,...,n1),若 a j a_j aj 的关键字 k j i = k k_j^i = k kji=k,就把 a j a_j aj 放进 Q k Q_k Qk 中。
  2. 收集:把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr1 各个队列中的节点依次首尾相接,得到新的结点队列,从而组成新的线性表。

通常采用链式基数排序。

空间效率:一趟排序需要的辅助存储空间为 r(r 个队头和 r 个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为 O ( r ) O(r) O(r)

时间效率:一趟分配需要 O ( n ) O(n) O(n),一趟收集需要 O ( r ) O(r) O(r)。所以时间复杂度为 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)),它与序列的初始状态无关。

稳定性:对于基数排序算法而言,按位排序时必须是稳定的。所以基数排序是稳定的。

8.7 内排序的比较及应用
算法种类最好情况平均情况最坏情况空间复杂度稳定性
直接插入排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
冒泡排序 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
简单选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)
希尔排序 O ( 1 ) O(1) O(1)
快速排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( l o g 2 n ) O(log_2n) O(log2n)
堆排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( 1 ) O(1) O(1)
2路归并排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n ) O(n) O(n)
基数排序 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( r ) O(r) O(r)

通常情况,对排序算法的比较和应用应考虑:

  1. 待排序元素数目 n。
  2. 元素本身信息量的大小。
  3. 关键字的结构及其分布情况。
  4. 稳定性的要求。
  5. 语言工具的条件,存储结构及辅助空间的大小等。

算法总结:

  1. 若 n 较小,可采用直接插入排序或简单选择排序。记录本身信息量较大时,选用简单选择排序较好(移动次数少)。
  2. 若文件的初始状态已按关键字基本有序,则可以选用直接插入排序或冒泡排序。
  3. 若 n 较大,则应选用时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的排序算法:快速排序、堆排序或归并排序。关键字随机分布时,快速排序平均时间最短。若要求稳定性,则可选用归并排序。也可改进归并为归并 + 插入以提高效率,且仍是稳定的。
  4. 基于比较的算法中,每次比较两个关键字的大小之后,尽可能出现两种转移,因此可以用一棵二叉树来描述比较判定过程。因此,任何借助比较的算法都至少需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 的时间。
  5. 若 n 很大,记录的关键字位数少且可以分解时,采用基数排序较好。
  6. 记录本身信息量较大时,为避免耗费大量时间移动记录,可采用链表作为存储结构。
8.8 examples

e.1

使用非递归方法实现快速排序时,通常需要一个栈记忆待排序区间的端点。能否用队列实现?

可以使用队列。

每一趟划分,可以把一个待排序区间分为两个子区间,然后分别对这两个子区间实行同样的划分。栈的作用是在处理一个子区间时,保存另一个子区间的上界和下界,待处理区间处理完后再取出下一个。队列也可实现这样的功能,只是再处理子区间时的顺序有所不同。

e.2

编写算法,能够在数组 L [ 1... n ] L[1...n] L[1...n] 中找出第 k 小的元素。

  1. 先排序,在直接找第 k 小的元素,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  2. 采用大根堆的方法,前 k 个元素建立大根堆,扫描剩余序列,若元素值比比堆顶数值小则弹出堆顶并插入当前元素,最终堆顶记为所求。时间复杂度为 O ( k + n l o g 2 k ) O(k+nlog_2k) O(k+nlog2k)
  3. 采用小根堆的方法,每次取出最小值元素,取 k 次,时间复杂度为 O ( n + k l o g 2 n ) O(n +klog_2n) O(n+klog2n)
  4. 基于快速排序的划分方法,选取枢轴并划分,不断递归。平均情况下可以达到 O ( n ) O(n) O(n),最坏情况 O ( n 2 ) O(n^2) O(n2)。空间复杂度取决于划分的方法。

注:枢轴思想解决问题时的平均情况下的复杂度为 O ( n ) O(n) O(n)

e.3

三色问题,给定一个由红、白、蓝三种颜色构成的序列,在 O ( n ) O(n) O(n) 的时间复杂度内完成对序列的重排,使序列变为“红 - 白 - 蓝”三色相连的情况。

设立三个指针,一个工作指针 j j j,一个指示红色 i i i,一个指示蓝色 k k k

实现:

typedef enum{RED, WHITE, BLUE} color; // 枚举数组
void Flag_Arrange(color a[], int n){
    int i = 0, j = 0, k = n - 1;
    while(j <= k){
        switch(a[j]){
            case RED: swap(a[i],a[j]); ++i; ++j; break;
            case WHITE: j++; break;
            case BLUE: swap(a[j], a[k]) --k;
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值