Chapter 7 排序(Sort)

文章目录

  • 1. 排序的概念及其算法性能
    • 1.1 概念
    • 1.2 排序算法的性能评估
    • 1.3 排序表的类定义
  • 2. 插入排序
    • 2.1 直接插入排序
      • 2.1.1 实现代码
      • 2.1.2 性能评估
    • 2.2 二分法插入排序
      • 2.2.1 实现代码
      • 2.2.2 性能评估
    • 2.3 希尔排序
      • 2.3.1 实现代码
      • 2.3.2 性能评估
  • 3. 快速排序
    • 3.1 实现代码
    • 3.2 性能评估 P407
  • 4. 选择排序
    • 4.1 直接选择排序
      • 4.1.1 代码实现
      • 4.1.2 性能评估 P414
    • 4.2 堆排序
      • 4.2.1 代码实现
      • 4.2.2 性能评估
  • 5. 归并排序
    • 5.1 代码实现
    • 5.2 性能评估
  • 6. 其他排序
    • 6.1 计数排序
    • 6.2 基数排序
    • 6.3 桶排序

1. 排序的概念及其算法性能


1.1 概念

数据表(dataList)
它是待排序元素的有限集合

排序码(key)
通常数据元素有多个属性域,即多个数据成员组成,其中有一个属性域可以用来区分元素,作为排序依据。该域即为排序码。

排序的确切定义
所谓排序,就是根据排序码递增或递减的顺序,把数据元素依次排列起来,使一组任意排列的元素变成一组按其排序码线性有序的元素。

排序算法的稳定性
如果在元素序列中有两个元素 R i R_i Ri R j R_j Rj,它们的排序码 K i = = K j K_i==K_j Ki==Kj,且在排序之前,元素 R i R_i Ri排在 R j R_j Rj前面。如果在排序之后,元素 R i R_i Ri仍在 R j R_j Rj前面,则称这个排序算法是稳定的,否则称这个排序算法不稳定。

内部排序和外排序
排序算法根据在排序过程中数据元素是否完全在内存,分为两大类:内部排序和外部排序。

1.2 排序算法的性能评估

排序算法的时间开销可用算法执行中的数据比较次数数据移动次数来衡量。
算法运行时间代价的大略估算一般都按平均情况进行估算,对于那些受元素排序码序列初始排列及元素个数影响较大的,需要按最好情况和最坏情况进行估算。

1.3 排序表的类定义

2. 插入排序


2.1 直接插入排序

2.1.1 实现代码

/**                                                                                                                                     
 * 直接插入排序
 */

#include 

using namespace std;

void Insert_Sort(int []);

int main(){
    int num[14] = {3,1,9,5,8,6,20,12,56,33,39,0,11,34};
    for (int i = 0; i < 14; i++){
        cout << num[i] << " ";
    }
    cout << endl;
    cout << endl;
    Insert_Sort(num);
    return 0;
}

void Insert_Sort(int n[]){
    int temp;
    for(int i = 1; i < 14; i++) {
        if (n[i] < n[i-1]){
            temp = n[i];
            int j = i-1;
            do {
                n[j+1] = n[j];
                
                ////////    
                for(int i = 0; i < 14; i++){
                    cout << n[i] << " ";
                }
                cout << endl;
                ////////
                
                j--;
            }while(j >= 0 && temp < n[j]);
            n[j+1] = temp;
            
            ////////    
            for(int i = 0; i < 14; i++){
                cout << n[i] << " ";
            }
            cout << endl;
            cout << endl;
            ////////
        }
    }
    cout << endl;
    for(int i = 0; i < 14; i++){
        cout << n[i] << " ";
    }
    cout << endl;
}

2.1.2 性能评估

设待排序的元素个数为 n n n,则该算法的主程序执行 n − 1 n-1 n1趟,因为排序码的比较次数和元素的移动次数与元素排序码的初始排列有关,so

  • 在最好情况下,排序码比较次数 K C N KCN KCN R M N RMN RMN分别为
    K C N = n − 1 KCN=n-1 KCN=n1
    R M N = 0 RMN=0 RMN=0
  • 在最坏情况下,排序码比较次数 K C N KCN KCN R M N RMN RMN分别为
    K C N = ∑ i = 1 n − 1 i = n ( n − 1 ) 2 ≈ n 2 2 KCN=\sum_{i=1}^{n-1}i=\frac{n(n-1)}{2}\approx \frac{n^2}{2} KCN=i=1n1i=2n(n1)2n2
    R M N = ∑ i = 1 n − 1 ( i + 2 ) = ( n + 4 ) ( n − 1 ) 2 ≈ n 2 2 RMN=\sum_{i=1}^{n-1}(i+2)=\frac{(n+4)(n-1)}{2}\approx \frac{n^2}{2} RMN=i=1n1(i+2)=2(n+4)(n1)2n2

以上讨论可知,直接插入排序的运行时间和待排序元素的原始排序顺序密切相关。若待排序元素序列中出现各种可能排列的概率相同,则可取上述最好最坏情况的平均情况。在平均情况下的 K C N KCN KCN R M N RMN RMN约为 n 2 4 \frac{n^2}{4} 4n2。因此,直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。直接插入排序是一种稳定的排序算法。

2.2 二分法插入排序

2.2.1 实现代码

/**                                                                                                                                     
 * 二分法插入排序(折半插入排序)
 */

#include 

using namespace std;

void Binary_Insert_Sort(int n[]);

int main(){
    int num[14] = {3,1,9,5,8,6,20,12,56,33,39,0,11,34};
	for (int i = 0; i < 14; i++){
        cout << num[i] << " ";
    }
    cout << endl;
    cout << endl;
    Binary_Insert_Sort(num);
    return 0;
}

void Binary_Insert_Sort(int n[]){                                                                                                       
    int temp,low,high,middle;
    for (int i = 1; i < 14; i++){
        temp = n[i];
        low = 0; //比较下界
        high = i-1;  //比较上界
        while (low <= high){
            middle = (low+high)/2;
            if (temp < n[middle])
                high = middle-1;
            else
                low = middle+1;
        }
        for (int k = i-1; k >= low; k--){
            n[k+1] = n[k];
        }
        n[low] = temp;
    }

    cout << endl;
    for(int i = 0; i < 14; i++){
        cout << n[i] << " ";
    }
    cout << endl;
}

2.2.2 性能评估

折半搜索比顺序搜索快,所以二分插入排序就平均性能来讲比直接插入排序要快。它所需要的 K C N KCN KCN与待排序元素序列的初始排列无关,仅依赖于元素的个数。在插入第 i i i个元素时,需经过 ⌊ l o g 2 i ⌋ + 1 \lfloor log_2i\rfloor+1 log2i+1次排序码比较,才能确定它应插入的位置。
因此,将n个元素(为推导方便设为 n = 2 k n=2^k n=2k)用折半插入排序的 K C N KCN KCN
K C N = ∑ i = 1 n − 1 ( ⌊ l o g 2 i ⌋ + 1 ) = ( 1 + 2 + 2 2 + . . . + 2 k − 1 ) + ( 2 + 2 2 + . . . + 2 k − 1 ) KCN=\sum_{i=1}^{n-1}(\lfloor log_2i\rfloor +1)=(1+2+2^2+...+2^{k-1})+(2+2^2+...+2^{k-1}) KCN=i=1n1(log2i+1)=(1+2+22+...+2k1)+(2+22+...+2k1)
= k ⋅ 2 k − 2 k + 1 = n ⋅ l o g 2 n − n + 1 ≈ n ⋅ l o g 2 n =k \cdot 2^k-2^k+1=n\cdot log_2n-n+1\approx n\cdot log_2n =k2k2k+1=nlog2nn+1nlog2n
n n n较大时,总 K C N KCN KCN比直接插入排序的最差情况要好得多,但比其最好情况要差。所以,在元素的初始序列已经按排序码排好或接近有序时,直接插入排序比折半插入排序执行的 K C N KCN KCN要少,折半插入排序的RMN与直接插入排序相同,依赖于元素的初始排列。折半插入排序是一个稳定的排序方法。

2.3 希尔排序

2.3.1 实现代码

/**
 * 希尔排序
 */

#include 

using namespace std;

void Shell_Sort(int n[],const int,const int);

int main(){
    int num[14] = {3,1,9,5,8,6,20,12,56,33,39,0,11,34};

    for (int i = 0; i < 14; i++){
        cout << num[i] << " ";
    }   
    cout << endl;
    cout << endl;
    Shell_Sort(num,0,13);
    return 0;
}

void Shell_Sort(int n[],const int left,const int right){
    int gap = right-left+1;
    int temp;
    do {
        gap = gap/3+1;
        
        /*这里使用直接插排*/
        for(int i = left+gap; i <= right; i++){
            if(n[i] < n[i-gap]){
                temp = n[i];
                int j = i-gap;
                do {
                    n[j+gap] = n[j];
                    j -=gap;
                }while(j >= left && temp < n[j]);
                n[j+gap] = temp;
            }

        }
        ////////
        for(int i = 0; i < 14; i++){
            cout << n[i] << " ";
        }
        cout << endl;
        cout << endl;
        ////////
    }while(gap > 1);

	cout << endl;
    for(int i = 0; i < 14; i++){
        cout << n[i] << " ";
    }
    cout << endl;   
}

2.3.2 性能评估

对希尔排序的时间复杂度的分析很困难,在特定情况下可以准确地估算 K C N KCN KCN R M N RMN RMN,但想要弄清 K C N KCN KCN R M N RMN RMN与增量选择之间的依赖关系并给出完整数学分析还没有人能做到。Knuth利用大量实验统计资料得出,当 n n n很大时,排序码平均比较次数和元素平均移动次数大约在 n 1.25 n^{1.25} n1.25 1.6 n 1.25 1.6n^{1.25} 1.6n1.25的范围内。这是在利用直接插排作为子序列排序算法的情况下得到的。

由于即使对于规模较大的序列( n ≤ 1000 n \leq 1000 n1000),希尔排序都具有很高的效率。并且希尔排序算法的代码简单,容易执行,所以很多排序应用程序都选用了希尔排序。希尔排序是一种不稳定的排序算法。

3. 快速排序


3.1 实现代码

快速排序是一种划分交换的方法,它采用分治法进行排序。

/**
 * 快速排序
 */

#include 
                                                                                                                                        
using namespace std;

void Quick_Sort (int n[],const int,const int);
int partition(int n[], const int low,const int high);

int main(){
    int num[14] = {3,1,9,5,8,6,20,12,56,33,39,0,11,34};

    for (int i = 0; i < 14; i++){
        cout << num[i] << " ";
    }   
    cout << endl;
    cout << endl;
    Quick_Sort(num, 0, 13);
    return 0;
}

void Quick_Sort (int n[], const int left, const int right){
    if(left < right){
        int pivotpos = partition(n, left, right); //划分
        Quick_Sort(n, left, pivotpos-1);
        Quick_Sort(n, pivotpos+1, right);
    }   

    cout << endl;
    for(int i = 0; i < 14; i++){
        cout << n[i] << " ";
    }
    cout << endl;
}

int partition(int n[], const int low,const int high){
    int pivotpos = low;
    int pivot = n[low];                 //基准元素
    for(int i = low+1; i <= high; i++)  //检测整个序列,进行划分
        if (n[i] < pivot){
            pivotpos++;
            if (pivotpos != i) swap(n[pivotpos], n[i]); //小于基准的交换到左边去
        }
    n[low] = n[pivotpos];
    n[pivotpos] = pivot;
    return pivotpos;
}

3.2 性能评估 P407

函数Quick_Sort的平均计算时间也是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),就平均计算时间而言,快排是我们所讨论的所有内部排序算法中最好的一个。

存储开销为 O ( l o g 2 n ) O(log_2n) O(log2n)

最坏情况,即待排序元素序列已经按其排序码从小到大排序的情况下,其递归树为单支树,总的排序码次数将达到
∑ i = 1 n − 1 ( n − i ) = 1 2 n ( n − 1 ) ≈ n 2 2 \sum_{i=1}^{n-1}(n-i)=\frac{1}{2}n(n-1)\approx \frac{n^2}{2} i=1n1(ni)=21n(n1)2n2
其排序速度退化到简单排序水平,比直接插入排序还慢。

(未完待续)

4. 选择排序


4.1 直接选择排序

4.1.1 代码实现

/**
 * 直接选择排序
 */

#include 

using namespace std;

void Select_Sort(int n[], const int, const int);                                                                                        
int main(){
    int num[14] = {3,1,9,5,8,6,20,12,56,33,39,0,11,34};
    
    for (int i = 0; i < 14; i++){
        cout << num[i] << " ";
    }   
    cout << endl;
    cout << endl;
    Select_Sort(num, 0, 13);

    return 0;
}

void Select_Sort(int n[],const int left, const int right){
    for (int i = left; i <= right; i++){
        int k = i;
        for (int j = i+1; j <=right; j++){
            if(n[j] < n[k]) 
                k = j;
        }
        if (k != i)
            swap(n[i], n[k]);
    }

    cout << endl;
    for(int i = 0; i < 14; i++){
        cout << n[i] << " ";
    }
    cout << endl;   
}

4.1.2 性能评估 P414

直接选择排序的排序码比较次数与元素序列的初始排列有关。当这组元素的初始状态是按其排序码从小到大有序的时候,元素移动次数 R M N = 0 RMN=0 RMN=0;而最坏的情况下是每一趟都要进行交换,总的 R M N = 3 ( n − 1 ) RMN=3(n-1) RMN=3(n1)。尽管如此,相比于其他排序算法,待排序元素序列的有序性对于选择排序的运行时间影响不大。

而且它对一类重要的元素序列具有较好的效率,这就是元素规模很大,而排序码却比较小的序列。因为对这种序列进行排序,移动操作所花费的时间要比比较操作的时间大得多,而其他算法移动操作的次数都要比选择排序来得多。直接选择排序是一种不稳定的排序方法。

4.2 堆排序

4.2.1 代码实现


4.2.2 性能评估

5. 归并排序


5.1 代码实现

5.2 性能评估

6. 其他排序


6.1 计数排序

6.2 基数排序

6.3 桶排序

你可能感兴趣的:(DataStructure)