1. 排序基础

一. 选择排序法 - Selection Sort

为什么要学习O(n^2)的排序算法?

1. 是基础
2. 编码简单,易于实现, 是一些简单情景的首选
3. 在一些特殊情况下, 简单的排序算法更有效
4. 简单的排序算法思想衍生出复杂的排序算法
5. 作为子过程,改进更复杂的排序算法

 

选择排序 Selection Sort

遍历每一个元素,每次遍历都找到被索引元素后面的最小元素,并和它交换位置。

c++实现SelectionSort.cpp

#include <iostream>
#include <algorithm>

using namespace std;

void selectionSort(int arr[], int n){

    for(int i = 0 ; i < n ; i ++){
        // 寻找[i, n)区间里的最小值
        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }

}

int main() {

    int a[10] = {10,9,8,7,6,5,4,3,2,1};
    selectionSort(a,10);
    for( int i = 0 ; i < 10 ; i ++ )
        cout<<a[i]<<" ";
    cout<<endl;

    return 0;
}


二. 使用模版(范型)编写算法

现在的已实现的算法最大的问题是 不支持范型(模版函数), 现在进行改进SelectionSort.cpp

#include <iostream>
#include "Student.h"

using namespace std;

template<typename T>
void selectionSort(T arr[], int n){

    for(int i = 0 ; i < n ; i ++){

        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }
}

int main() {

    // 测试模板函数,传入整型数组
    int a[10] = {10,9,8,7,6,5,4,3,2,1};
    selectionSort( a , 10 );
    for( int i = 0 ; i < 10 ; i ++ )
        cout<<a[i]<<" ";
    cout<<endl;

    // 测试模板函数,传入浮点数数组
    float b[4] = {4.4,3.3,2.2,1.1};
    selectionSort(b,4);
    for( int i = 0 ; i < 4 ; i ++ )
        cout<<b[i]<<" ";
    cout<<endl;

    // 测试模板函数,传入字符串数组
    string c[4] = {"D","C","B","A"};
    selectionSort(c,4);
    for( int i = 0 ; i < 4 ; i ++ )
        cout<<c[i]<<" ";
    cout<<endl;

    // 测试模板函数,传入自定义结构体Student数组
    Student d[4] = { {"D",90} , {"C",100} , {"B",95} , {"A",95} };
    selectionSort(d,4);
    for( int i = 0 ; i < 4 ; i ++ )
        cout<<d[i];
    cout<<endl;

    return 0;
}

student.h 存放可以比较的自定义类

#ifndef INC_02_SELECTION_SORT_USING_TEMPLATE_STUDENT_H
#define INC_02_SELECTION_SORT_USING_TEMPLATE_STUDENT_H

#include <iostream>
#include <string>

using namespace std;


struct Student{

    string name;
    int score;

    // 重载小于运算法,定义Student之间的比较方式
    // 如果分数相等,则按照名字的字母序排序
    // 如果分数不等,则分数高的靠前
    bool operator<(const Student& otherStudent){
        return score != otherStudent.score ?
               score > otherStudent.score : name < otherStudent.name;
    }

    // 重载<<符号, 定义Student实例的打印输出方式
    // * 很多同学看到这里的C++语法, 头就大了, 甚至还有同学表示要重新学习C++语言
    // * 对于这个课程, 大可不必。C++语言并不是这个课程的重点,
    // * 大家也完全可以使用自己的方式书写代码, 最终只要能够打印出结果就好了, 比如设置一个成员函数, 叫做show()...
    // * 推荐大家阅读我在问答区向大家分享的一个学习心得: 【学习心得分享】请大家抓大放小,不要纠结于C++语言的语法细节
    // * 链接: http://coding.imooc.com/learn/questiondetail/4100.html
    friend ostream& operator<<(ostream &os, const Student &student){

        os<<"Student: "<<student.name<<" "<<student.score<<endl;
        return os;
    }
};

#endif //INC_02_SELECTION_SORT_USING_TEMPLATE_STUDENT_H

测试结果

1 2 3 4 5 6 7 8 9 10
1.1 2.2 3.3 4.4
A B C D
Student: C 100
Student: A 95
Student: B 95
Student: D 90

三. 随机生成算法测试用例

在上一节SelectionSort的main函数中进行的测试,测试量太小, 我们本节来编写一个随机生成算法测试用例。

新建SortTestHelper.h

#ifndef INC_03_SELECTION_SORT_GENERATE_TEST_CASES_SORTTESTHELPER_H
#define INC_03_SELECTION_SORT_GENERATE_TEST_CASES_SORTTESTHELPER_H

#include <iostream>
#include <ctime>
#include <cassert>

using namespace std;


namespace SortTestHelper {

    // 生成有n个元素的随机数组,每个元素的随机范围为[rangeL, rangeR], 返回一个数组
    int *generateRandomArray(int n, int rangeL, int rangeR) {

        assert(rangeL <= rangeR);//如果条件不成立,程序终止

        int *arr = new int[n];

        srand(time(NULL));
        for (int i = 0; i < n; i++)
            arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;//较为标准的c++生成范围内的随机数方式
        return arr;
    }

    // 打印arr数组的所有内容
    template<typename T>
    void printArray(T arr[], int n) {

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

        return;
    }

};
#endif //INC_03_SELECTION_SORT_GENERATE_TEST_CASES_SORTTESTHELPER_H

再次进行测试: main.cpp(SelectionSort.cpp修改而来)

#include <iostream>
#include "SortTestHelper.h"

using namespace std;

template<typename T>
void selectionSort(T arr[], int n){

    for(int i = 0 ; i < n ; i ++){

        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }
}

int main() {

    // 测试排序算法辅助函数
    int N = 20000;
    int *arr = SortTestHelper::generateRandomArray(N,0,100000);
    selectionSort(arr,N);
    SortTestHelper::printArray(arr,N);
    delete[] arr;

    return 0;
}

测试结果

5 13 16 16 22 31 35 37 43 49 56 57 80 81 82 83 85 85 89 99 103 109 116 117
117 133 143 144 152 155 167 167 171 174 178 179 181 187 200 201 205 211 220
224 236 240 248 252 254 256 259 259 261 266 274 277 279 280 303 308 308 309
310 314 318 320 321 337 346 348 350 365 370 372 376 376 380 390 393 401 419
 ....99976 99985 99986 99988

以后的测试,都会按照这样随机生成测试用例的方式来进行


四. 测试算法的性能

测试算法的性能方法一般都是通过比较运行的时间。
在SortTestHelper.h中增加相关方法

...

namespace SortTestHelper {

    ...
    
    // 判断arr数组是否有序
    template<typename T>
    bool isSorted(T arr[], int n) {

        for (int i = 0; i < n - 1; i++)
            if (arr[i] > arr[i + 1])
                return false;

        return true;
    }

    // 测试sort排序算法排序arr数组所得到结果的正确性和算法运行时间
    template<typename T>
    void testSort(const string &sortName, void (*sort)(T[], int), T arr[], int n) {

        clock_t startTime = clock();//获取时钟周期
        sort(arr, n);
        clock_t endTime = clock(); //endTime - startTime得到函数执行 经过的时钟周期个数

        assert(isSorted(arr, n));// 验证排序后的数组是否有序, 判断排序算法是否正确
        cout << sortName << " : " << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl;
        //CLOCKS_PER_SEC  标准库中  表示 每秒 时钟周期的个数
        return;
    }

};
#endif 

在main.cpp中测试

...

int main() {

    int n = 20000;
    int *arr = SortTestHelper::generateRandomArray(n,0,n);
    SortTestHelper::testSort("Selection Sort", selectionSort, arr, n);
    delete[] arr;

    return 0;
}

结果

Selection Sort : 0.362772 s

五. 插入排序法- Insertion Sort

插入排序:
    遍历每个元素, 将遍历到的元素插入到它前面元素中合适的位置
    
举例:
    假设对'[ 8   6   2   3 ]'进行排序
遍历到:
    8          位置不变
    6      6   8    2   3
    2      6   2    8   3
           2   6    8   3
    3      2   6    3   8
           2   3    6   8
                结束

先将之前的选择排序放入SelectionSort.cpp

#ifndef INC_04_INSERTION_SORT_SELECTIONSORT_H
#define INC_04_INSERTION_SORT_SELECTIONSORT_H

#include <iostream>
#include <algorithm>

using namespace std;


template<typename T>
void selectionSort(T arr[], int n){

    for(int i = 0 ; i < n ; i ++){

        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }
}

#endif 

在main.cpp中编写插入排序

#include <iostream>
#include <algorithm>
#include "SortTestHelper.h"
#include "SelectionSort.h"

using namespace std;

template<typename T>
void insertionSort(T arr[], int n){

    for( int i = 1 ; i < n ; i ++ ) {

        // 寻找元素arr[i]合适的插入位置
        // 写法1
//        for( int j = i ; j > 0 ; j-- )
//            if( arr[j] < arr[j-1] )
//                swap( arr[j] , arr[j-1] );
//            else
//                break;

        // 写法2
        for( int j = i ; j > 0 && arr[j] < arr[j-1] ; j -- )
            swap( arr[j] , arr[j-1] );

    }

    return;
}

// 比较SelectionSort和InsertionSort两种排序算法的性能效率
// 此时, 插入排序比选择排序性能略低
int main() {

    int n = 20000;

    cout<<"Test for random array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int *arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int *arr2 = SortTestHelper::copyIntArray(arr1, n);//拷贝一份相同的数组

    SortTestHelper::testSort("Insertion Sort", insertionSort,arr1,n);
    SortTestHelper::testSort("Selection Sort", selectionSort,arr2,n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;

    return 0;
}

在SortTestHelper.h中补上copyIntArray方法

...


namespace SortTestHelper {

...

    // 拷贝整型数组a中的所有元素到一个新的数组, 并返回新的数组
    int *copyIntArray(int a[], int n){

        int *arr = new int[n];
        //* 在VS中, copy函数被认为是不安全的, 请大家手动写一遍for循环:)
        copy(a, a+n, arr);
        return arr;
    }

...

运行main.cpp

Test for random array, size = 20000, random range [0, 20000]
Insertion Sort : 0.562364 s
Selection Sort : 0.375467 s
插入排序理论上 比选择排序  更有机会提前结束排序
但我们发现运行结果 插入排序反而比选择排序慢, 这是为什么呢?

六. 插入排序的改进

上一节中:插入排序比选择排序慢, 这是为什么呢?

答: 插入排序有更多的交换操作 swap, 一次交换操作 = 三次赋值操作

插入排序优化方案

思路: 赋值操作代替交换操作
以 对'[2  6  8   3]'进行排序, 正好遍历到3的位置为例:

之前的方案:    
            1.     8 > 3      8  和  3交换   变成   2 6 3 8
            2.     6 > 3      6  和  3交换   变成   2 3 6 8
            3.     2 < 3      结束

现在的方案:
            1.     用变量e先暂时保存3
            2.     8 > 3     将3原来的位置赋值为8 变成   2 6 8 8
            3.     6 > 3     将8原来的位置赋值为6 变成   2 6 6 8
            4.     2 < 3     将6原来的位置赋值为e 变成   2 3 6 8  结束

更具以上逻辑 修改main.cpp中的插入排序代码

#include <iostream>
#include <algorithm>
#include "SortTestHelper.h"
#include "SelectionSort.h"

using namespace std;

template<typename T>
void insertionSort(T arr[], int n){

    for( int i = 1 ; i < n ; i ++ ) {

        // 寻找元素arr[i]合适的插入位置
        // 写法1
//        for( int j = i ; j > 0 ; j-- )
//            if( arr[j] < arr[j-1] )
//                swap( arr[j] , arr[j-1] );
//            else
//                break;

        // 写法2
//        for( int j = i ; j > 0 && arr[j] < arr[j-1] ; j -- )
//            swap( arr[j] , arr[j-1] );

        // 写法3
        T e = arr[i];
        int j; // j保存元素e应该插入的位置
        for (j = i; j > 0 && arr[j-1] > e; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }

    return;
}

int main() {

...
}
插入排序 对 有序性越高的 数据, 排序速度越快

我们完善main.cpp中的main测试函数

int main() {

    int n = 20000;
    
    // 测试1 一般测试
    cout<<"Test for random array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
    int *arr1 = SortTestHelper::generateRandomArray(n,0,n);
    int *arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort,arr1,n);
    SortTestHelper::testSort("Selection Sort", selectionSort,arr2,n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 测试2 有序性更强的测试
    cout<<"Test for more ordered random array, size = "<<n<<", random range [0, 3]"<<endl;
    arr1 = SortTestHelper::generateRandomArray(n,0,3);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort,arr1,n);
    SortTestHelper::testSort("Selection Sort", selectionSort,arr2,n);

    delete[] arr1;
    delete[] arr2;

    cout<<endl;


    // 测试3 测试近乎有序的数组
    int swapTimes = 100;
    cout<<"Test for nearly ordered array, size = "<<n<<", swap time = "<<swapTimes<<endl;
    arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
    arr2 = SortTestHelper::copyIntArray(arr1, n);

    SortTestHelper::testSort("Insertion Sort", insertionSort,arr1,n);
    SortTestHelper::testSort("Selection Sort", selectionSort,arr2,n);

    delete[] arr1;
    delete[] arr2;

    return 0;
}

在SortTestHelper中增加生成近乎有序的数组的方法generateNearlyOrderedArray

// 生成一个近乎有序的数组
    // 首先生成一个含有[0...n-1]的完全有序数组, 之后随机交换swapTimes对数据
    // swapTimes定义了数组的无序程度:
    // swapTimes == 0 时, 数组完全有序
    // swapTimes 越大, 数组越趋向于无序
    int *generateNearlyOrderedArray(int n, int swapTimes){

        int *arr = new int[n];
        for(int i = 0 ; i < n ; i ++ )
            arr[i] = i;     

        srand(time(NULL));
        for( int i = 0 ; i < swapTimes ; i ++ ){
            int posx = rand()%n;
            int posy = rand()%n;
            swap( arr[posx] , arr[posy] );
        }

        return arr;
    }

运行main.cpp进行测试

Test for random array, size = 20000, random range [0, 20000]
Insertion Sort : 0.281641 s
Selection Sort : 0.376118 s

Test for more ordered random array, size = 20000, random range [0, 3]
Insertion Sort : 0.197291 s
Selection Sort : 0.385193 s

Test for nearly ordered array, size = 20000, swap time = 100
Insertion Sort : 0.00327 s
Selection Sort : 0.387481 s

结论

可以看到插入排序优势明显, 且原数据有序性越强, 插入排序速度越快。

所以插入排序 实际生产中, 也具有很高的可用性

七. 更多关于O(n^2)排序算法的思考

冒泡排序

除了    选择排序(Selection Sort)   和  插入排序(Insertion Sort)外
在大学中接触的第一个排序算法: 冒泡排序(Bubble Sort)也是很常见的排序

这三种排序中, 插入排序的效率是最高的

冒泡排序的过程举例

对 [6, 3, 8, 2, 9, 1]进行冒泡排序

第一轮排序:

    第一次比较  6和3比较 结果:3    6   8   2   9   1     

    第二次比较  6和3比较 结果:3    6   8   2   9   1 

    第三次比较  8和2比较 结果:3    6   2   8   9   1 

    第四次比较  8和9比较 结果:3    6   2   8   9   1 

    第五次比较  9和1比较 结果:3    6   2   8   1   9 

  第一轮比较总结:1.排序第1轮、比较5次,没有获得从小到大的排序   2.因为每次比较都是大数往后靠,所以比较完成后,可以确定大数排在最后(9 已经冒泡冒出来了,下轮比较可以不用比较了 )

 

  第二轮排序:

    第一次比较  3和6比较 结果:3    6   2   8   1   9     

    第二次比较  6和2比较 结果:3    2   6   8   1   9 

    第三次比较  6和8比较 结果:3    2   6   8   1   9 

    第四次比较  8和1比较 结果:3    2   6   1   8   9 

 

  第二轮比较总结:1.排序第2轮、比较4次,没有获得从小到大的排序   2.冒泡出了 8,下轮不用比较8 了

  

  第三轮排序:

    第一次比较  3和2比较 结果:2    3   6   1   8   9     

    第二次比较  3和6比较 结果:2    3   6   1   8   9 

    第三次比较  6和1比较 结果:2    3   1   6   8   9 

  第三轮比较总结:1.排序第3轮、比较3次,没有获得从小到大的排序   2.冒泡出了 6,下轮不用比较6 了

 

  第四轮排序:

    第一次比较  2和3比较 结果:2    3   1   6   8   9     

    第二次比较  3和1比较 结果:2    1   3   6   8   9 

  第四轮比较总结:1.排序第4轮、比较2次,没有获得从小到大的排序   2.冒泡出了 3,下轮不用比较3 了

 

  第五轮排序:

    第一次比较  2和1比较 结果:1   2   3   6   8   9     

  第五轮比较总结:1.排序第5轮、比较1次,没有获得从小到大的排序   2.冒泡出了 2,由于还剩一个1,不用再比较了,至此通过5轮排序,完成整个排序。

 

  通过以上五轮排序,若干次比较,我们有理由推断出一个结论:

  对于一个长度为N的数组,我们需要排序 N-1 轮,每 i 轮 要比较 N-i 次。对此我们可以用双重循环语句,外层循环控制循环轮次,内层循环控制每轮的比较次数。

 

希尔排序

希尔排序(Shell Sort)也是插入排序的一种。
也称为缩小增量排序,是直接插入排序算法的一种更高效的改进版本。
希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。

基本思想:

  将待排序列划分为若干组,在每一组内进行插入排序,以使整个序列基本有序,然后再对整个序列进行插入排。

举例:
下标 0  1  2  3  4  5  6  7  8  9
数组 49 38 65 97 26 13 27 50 55 4 (原数组)
 
增量=5, [0]=49与[5]=13为一组,互换为 13 49 (排序是从小到大)
        [1]=38与[6]=27为一组,互换为 27 38 
        [2]=65与[7]=50为一组,互换为 50 65
        [3]=97与[8]=55为一组,互换为 55 97
        [4]=26与[9]=4 为一组,互换为 4 26
 
增量=5的排序结果是: 13 27 50 55 4 49 38 65 97 26
 
 
下标  0  1  2  3  4  5  6  7  8  9
数组  13 27 50 55 4  49 38 65 97 26 (第一趟之后)
 
增量=2, [0]=13,[2]=50,[4]=4,[6]=38,[8]=97为一组,
        互换之后,[0]=4,[2]=13,[4]=38,[6]=50,[8]=97
 
        [1]=27,[3]=55,[5]=49,[7]=65,[9]=26为一组,
        互换之后,[1]=26,[3]=27,[5]=49,[7]=55,[9]=65
 
增量=2的排序结果是: 4 26 13 27 38 49 50 55 97 65
 
 
下标  0  1  2  3  4  5  6  7  8  9
数组  4  26 13 27 38 49 50 55 97 65 (第二趟之后)
 
增量=1, 数组里的10个数据作为一组,其中,
        [1]=26有[2]=13互换为 13 26
        [8]=97与[9]=65互换为 65 97
 
增量=1的排序结果是: 4 13 26 27 38 49 50 55 65 97

 

为了更方便的阅读排序的逻辑, 在最后附上多种排序算法的python版本:

from random import randint
import timeit


def bubbleSort(alist):
    exchange = False
    for i in range(len(alist)-1, 0, -1):
        for j in range(i):
            if alist[j] > alist[j+1]:
                alist[j], alist[j+1] = alist[j+1], alist[j]
                exchange = True
        if not exchange:
            break
    return alist


def selectionSort(alist):
    for i in range(len(alist)):
        minposition = i
        for j in range(i, len(alist)):
            if alist[minposition] > alist[j]:
                minposition = j
        alist[i], alist[minposition] = alist[minposition], alist[i]
    return alist


def insertionSort(alist):
    for i in range(1, len(alist)):
        currentvalue = alist[i]
        position = i
        while alist[position-1] > currentvalue and position > 0:
            alist[position] = alist[position-1]
            position = position-1
        alist[position] = currentvalue
    return alist


def shellSort(alist):
    gap = len(alist)//2
    while gap > 0:
        for startpos in range(gap):
            gapInsertionSort(alist, startpos, gap)
        gap = gap//2
    return alist


def gapInsertionSort(alist, startpos, gap):
    # 希尔排序的辅助函数
    for i in range(startpos+gap, len(alist), gap):
        position = i
        currentvalue = alist[i]
        while position >= gap and alist[position-gap] > currentvalue:
            alist[position] = alist[position-gap]
            position = position-gap
        alist[position] = currentvalue


max = 5000
list = [randint(-max, max) for x in range(max)]
# 使用切片可以真正将一份list复制给其他变量,如果不用切片,即alist=list,只是指针而已。
alist = list[:]
blist = list[:]
clist = list[:]
dlist = list[:]

'''
运行次数(number)只能设置成1,因为内存中alist、blist等指向同一个对象,该对象第一次排序后就已经是有序列表了。
所以在这种情况下会发生有趣的现象。按照短路冒泡排序的性质,它在碰到一个有序列表以后会立刻停止遍历,所以不管它的number是1还是10,time都几乎没变化
但其他排序方法,就算对有序列表进行排序,交换是不需要了,但是还要遍历&比较,所以他们的运行次数变多的话,time依旧变大
之前我就是把number设置成100,发现短路冒泡排序简直太快了,才发现这个问题。
'''
t1 = timeit.Timer('bubbleSort(alist)', 'from __main__ import bubbleSort,alist')
print('短路冒泡排序: %s s' % t1.timeit(number=1))

t2 = timeit.Timer('selectionSort(blist)',
                  'from __main__ import selectionSort,blist')
print('选择排序: %s s' % t2.timeit(number=1))

t3 = timeit.Timer('insertionSort(clist)',
                  'from __main__ import insertionSort,clist')
print('插入排序: %s s' % t3.timeit(number=1))

t4 = timeit.Timer('shellSort(dlist)', 'from __main__ import shellSort,dlist')
print('希尔排序: %s s' % t4.timeit(number=1))

运行结果

短路冒泡排序: 2.05309104919 s
选择排序: 0.866323947906 s
插入排序: 0.928652048111 s
希尔排序: 0.0225701332092 s

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/81380343