常见排序算法总结(基于C++实现)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hf19931101/article/details/79077874

1.插入排序

1.1 直接插入

基本思想:
将待排序表看作左右两部分,其中左边为有序区,右边为无序区,整个排序过程就是将右边无序区中的元素逐个插入到左边的有序区中,以构成新的有序区。

template<typename T>
void InsertSort(T A[], int n) {
    for (int i = 1; i < n; i++)
    {
        T temp = A[i];
        int j = i - 1;
        while (A[j] > temp && j >= 0)
        {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = temp;
    }
}

1.2 希尔排序

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

分组方法:
对给定的一个步长d(d>0),将下标相差为d的倍数的元素分在一组。d的取值依次为: d1=n/2, d2=d1/2, ……,dk=1

template<typename T>
void ShellSort(T A[], int n) {
    int d = n / 2;     //步长初始为n/2
    while (d > 0) {
        for (int i = d; i < n; i++) {
            T x = A[i];
            int j = i - d;
            while (j >= 0 && x < A[j]) {
                A[j + d] = A[j];
                j = j - d;
            }
            A[j + d] = x;
        }
        d = d / 2;    //步长变为原来的1/2
    }
}

1.3 小结

排序方法 时间复杂度 空间复杂度(辅助空间) 稳定性
直接插入 平均情况 O(n^2) 最坏情况 O(n^2) 最好情况 O(n) O(1) 稳定
希尔排序 平均情况 O(n*Log2(n)) 最坏情况 O(n^2) 最好情况 O(n) O(1) 不稳定

2.选择排序

2.1 冒泡排序

基本思想:
从一端开始,逐个比较相邻的两个元素,发现倒序即交换。这里按从后往前(从下往上)逐个比较相邻元素。

template<typename T>
void BubbleSort(T* elements, int n)
{
    bool exchange = true;
    for (int i=0; i<n && exchange; i++)
    {
        exchange = false;
        for (int j=n-1; j>i; j--)
        {
            if (elements[j] < elements[j-1])
            {
                T temp = elements[j];
                elements[j] = elements[j - 1];
                elements[j - 1] = temp;
                exchange = true;
            }
        }
    }
}

2.2 快速排序

基本思想:
将数据划分为两部分,左边的所有元素都小于右边的所有元素;然后,对左右两边进行快速排序。

划分方法:
选定一个参考点(中间元素),所有元素与之相比较,小的放左边,大的放右边。

template<typename T>
void  partition(T a[], int low, int high, int &curPoint)
{
    T mid = a[low];         //选定数组的首元素作为中间元素
    int i = low, j = high;  //i是数组的头,j是数组的尾
    while (i != j)
    {
        //把右边的比中间元素小的元素放到左边
        while (i < j && a[j] > mid) { j--; }
        a[i] = a[j];    //if (i < j) { a[i] = a[j]; i++; }
        //把左边的比中间元素大的元素放到右边
        while (i < j && a[i] < mid) { i++; }
        a[j] = a[i];    //if (i < j) { a[j] = a[i]; j--; }
    }
    //把中间元素放到中间
    a[i] = mid;
    //返回中间元素的位置
    curPoint = i;
}

//对数组下标为low~high的子表进行快速排序
template<typename T>
void QuickSort(T a[], int low, int high)
{
    int i;
    if (low < high)
    {
        partition(a, low, high, i);
        QuickSort(a, low, i - 1);
        QuickSort(a, i + 1, high);
    }
}

一个更简洁的实现如下:

template<typename T>
void QuickSort(int b, int e, T a[]) 
{ 
    int i = b, j = e;  //i是数组的头,j是数组的尾
    T x = a[(b + e) / 2]; //选取数组的中间元素作为划分的基准
    while (i<j)
    {
        while (a[i]<x) i++; 
        while (a[j]>x) j--; 
        if (i <= j) 
            std::swap(a[i++], a[j--]); 
    }
    if (i<e) 
        QuickSort(i, e, a);
    if (j>b) 
        QuickSort(b, j, a);
}

2.3 小结

排序方法 时间复杂度 空间复杂度(辅助空间) 稳定性
冒泡排序 平均情况 O(n^2) 最坏情况 O(n^2) 最好情况 O(n) O(1) 稳定
快速排序 平均情况 O(nLog2(n)) 最坏情况 O(n^2) 最好情况 O(nLog2(n)) O(1) 不稳定

3.选择排序

基本思想:
在每一趟排序中,从待排序子表中选出关键字最小(大)的元素放在其最终位置上。

3.1 直接选择排序

基本思想:
在待排序子表中找出最大(小)元素,并将该元素放在子表的最前(后)面。

template<typename T>
void SelectSort(T A[], int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        T min = i;
        //找到未排序子表中最小数的位置
        for (int j = i + 1; j < n; j++)
            if (A[j] < A[min])
                min = j;
        //将当前的最小数放到其位置上
        if (min != i)
        {
            T temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}

3.2 树形选择排序

基本思想:

1.树形选择排序(Tree Selection Sort),是一种按照锦标赛的思想进行选择排序的方法。

2.首先对n个记录进行两两比较,然后优胜者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。这个过程可以用一棵有n个叶子结点的完全二叉树表示。根节点中的关键字即为叶子结点中的最小关键字。

3.在输出最小关键字之后,欲选出次小关键字,仅需将叶子结点中的最小关键字改为“最大值”,如∞,然后从该叶子结点开始,和其兄弟的关键字进行比较,修改从叶子结点到根的路径上各结点的关键字,最后根结点的关键字即为次小关键字。

template<typename T>
void TreeSelectionSort(T data[], int n)
{
    T MinValue = -100000000;  //用该数表示负无穷
    int baseSize;             //刚好能存储n个数的最小的2的幂,等于满二叉树最下一层的叶子树
    int treeSize;             //整个二叉树的节点数
    int i;
    T max;
    int maxIndex;

    baseSize = 1;
    while (baseSize < n)
    {
        baseSize *= 2;
    }
    //用最下一层的叶子树计算总节点数
    treeSize = baseSize * 2 - 1;

    //创建数组存放二叉树,数据从下标1开始存放
    T* tree = new T[treeSize + 1]();

    //将数据放入最下一层中
    for (i = 0; i < n; i++)
    {
        tree[treeSize - i] = data[i];
    }
    //将二叉树的其他节点初始化为负无穷,如果每轮是求一个最小值则应初始化为正无穷
    for (; i < baseSize; i++)
    {
        tree[treeSize - i] = MinValue;
    }
    // 构造一棵树,根节点是最大值
    for (i = treeSize; i > 1; i -= 2)
    {
        tree[i / 2] = (tree[i] > tree[i - 1] ? tree[i] : tree[i - 1]);
    }
    n -= 1;    //未排序的数量-1
    while (n >= 0)
    {
        max = tree[1];             //取出最大值
        data[n--] = max;          //将当前找到的最大值放到数组的最后面
        //在二叉树的最下面一层找到当前最大值的位置
        maxIndex = treeSize;
        while (tree[maxIndex] != max)
        {
            maxIndex--;
        }
        tree[maxIndex] = MinValue;  //将树中底层的现最大值替换为负无穷

        //从被替换的位置向上计算,修改从叶子结点到根的路径上各结点的值
        while (maxIndex > 1)
        {
            if (maxIndex % 2 == 0)
            {
                tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex + 1] ? tree[maxIndex] : tree[maxIndex + 1]);
            }
            else
            {
                tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex - 1] ? tree[maxIndex] : tree[maxIndex - 1]);
            }
            maxIndex /= 2;
        }
    }
    delete[] tree;
}

3.3 堆排序

实现堆排序还需要解决两个问题:

(1)如何由一个无序序列建成一个堆?

(2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

问题(2)的解决方法是:

在输出堆顶元素之后,以堆中最后一个元素替代之,此时根结点的左、右子树均为堆,则仅需自上至下进行调整即可。
我们称自堆顶至叶子的调整过程为“筛选”。

问题(1)的解决方法是:

从一个无序序列建堆的过程就是一个反复“筛选”的过程。若将此序列看成是一个完全二叉树,则最后一个非终端结点是第⌞n/2⌟个元素,由此“筛选”只需从第⌞n/2⌟个元素开始。

一个基于大根堆的升序排序算法的简单实现如下:

//将子序列(下标范围:k~size)调整为大顶堆
template<typename T>
void Sift(T A[], int k, int size)
{
    T x = A[k];  //k是待调整的子树的根
    int i = k      //i指示空位
    int j = 2*i;   //j是i的左孩子
    bool fininshed = false;
    while (j <= size && !fininshed)
    {
        //让j指向i的左右孩子中的较大者
        if (j < size && A[j] < A[j + 1]) j = j + 1;

        //如果根最大则直接结束
        if (x >= A[j]) fininshed = true;
        //否则,孩子节点比根节点大
        else
        {
            A[i] = A[j];   //大的孩子节点上移
            //继续往下筛选
            i = j;
            j = 2 * i;
        }
    }
    A[i] = x;
}

//通过大顶堆对数组A进行升序排序,数据存放在下标1~n;
//实际上数组尺寸为n+1,这样做是为了便于二叉树的处理,使得根节点的下标为1
template<typename T>
void HeapSort(T A[], int n)
{
    //构建初始堆
    for (int i = n / 2; i > 0; i--)
    {
        sift(A, i, n);
    }

    for (int i = n; i > 1; i--)
    {
        T x = A[1];    //输出根
        A[1] = A[i];   //交换当前最后一个元素和根的值
        A[i] = x;
        sift(A, 1, i - 1);  //调整子序列为堆
    }
}

使用函数比较大小可以方便地切换大/小顶堆,代码如下:

template<typename T>
bool fMax(const T& a, const T& b)
{
    return a >= b;
}

template<typename T>
bool fMin(const T& a, const T& b)
{
    return a <= b;
}

//将子序列(下标范围:k~size)调整为(大/小)顶堆
template<typename T>
void Sift(T A[], int k, int size, bool (*compare)(const T&, const T&))
{
    T x = A[k];    //k是待调整的子树的根
    int i = k;     //i指示空位
    int j = 2*i;   //j是i的左孩子
    bool fininshed = false;
    while (j <= size && !fininshed)
    {
        //让j指向i的左右孩子中的较(大/小)者
        if (j < size && !compare(A[j], A[j + 1])) j = j + 1;

        //如果根最(大/小)则直接结束
        if (compare(x, A[j])) fininshed = true;
        //否则,孩子节点比根节点(大/小)
        else
        {
            A[i] = A[j];   //(大/小)的孩子节点上移
            //继续往下筛选
            i = j;
            j = 2 * i;
        }
    }
    A[i] = x;
}

//通过(大/小)顶堆对数组A进行(升/降)序排序,数据存放在下标1~n;实际上数组尺寸为n+1,这样做是为了便于二叉树的处理,使得根节点的下标为1
template<typename T>
void HeapSort(T A[], int n, bool(*compare)(const T&, const T&))
{
    //构建初始堆
    for (int i = n / 2; i > 0; i--)
    {
        Sift(A, i, n, compare);
    }

    for (int i = n; i > 1; i--)
    {
        T x = A[1];    //输出根
        A[1] = A[i];   //交换当前最后一个元素和根的值
        A[i] = x;
        Sift(A, 1, i - 1, compare);  //调整子序列为堆
    }
}

int main()
{
    int A[16] = { -1, 5,3,8,10,2,4,1,9,7,6,15,13,11,14,12};
    HeapSort(A, sizeof(A) / sizeof(int) - 1, fMax); //使用大顶堆进行升序排序
    HeapSort(A, sizeof(A) / sizeof(int) - 1, fMin);//使用小顶堆进行降序排序
    return 0;
}

3.4 小结

排序方法 时间复杂度 空间复杂度(辅助空间) 稳定性
直接选择排序 平均情况 O(n^2) 最坏情况 O(n^2) 最好情况 O(n^2) O(1) 不稳定
树形选择排序 平均情况 O(nLog2(n)) 最坏情况 O(nLog2(n)) 最好情况 O(nLog2(n)) O(n-1) 不稳定
堆排序 平均情况 O(nLog2(n)) 最坏情况 O(nLog2(n)) 最好情况 O(nLog2(n)) O(1) 不稳定

4. 归并排序

基本思想:
是指将两个或两个以上的有序表合并成一个新的有序表。

利用归并的思想进行排序:

  1. 首先将整个表看成是n个有序子表,每个子表的长度为1;

  2. 然后两两归并,得到n/2个长度为2的有序子表;

  3. 然后再两两归并,直至得到一个长度为n的有序表为止。

//合并数组A中以a和b开始的长度为step的两个子序列,n为整个数组的长度
template<typename T>
void Merge(T A[], int start, int mid, int step, int n)
{
    int rightLen = 0;
    if (mid + step > n)
    {
        rightLen = n - mid;
    }
    else
    {
        rightLen = step;
    }
    //申请空间存放临时结果
    T *tempArray = new T[step + rightLen]();
    int i = 0, j = 0, k = 0;
    while (i < step && j < rightLen)
    {
        if (A[start + i] < A[mid + j])
        {
            tempArray[k++] = A[start + i];
            i++;
        }
        else
        {
            tempArray[k++] = A[mid + j];
            j++;
        }
    }
    //如果左边没有归并完,那么直接将剩余的元素复制到tempArray的后面
    if (j == rightLen)
    {
        memcpy(tempArray + i + j, A + start + i, (step - i) * sizeof(T));
    }
    //如果右边没有归并完,那么直接将剩余的元素复制到tempArray的后面
    else if (i == step)
    {
        memcpy(tempArray + i + j, A + mid + j, (rightLen - j) * sizeof(T));
    }
    //将临时数组中排好序的内容复制回原数组
    memcpy(A + start, tempArray, (step + rightLen) * sizeof(T));
    delete[] tempArray;
}

template<typename T>
void MergeSort(T A[], int n)
{
    for (int step = 1; step < n; step *= 2)
    {
        for (int i = 0; i < n - step; i += 2*step)
        {
            Merge(A, i, i+step, step, n);
        }
    }
}

小结:

排序方法 时间复杂度 空间复杂度(辅助空间) 稳定性
归并排序 平均情况 O(nLog2(n)) 最坏情况 O(nLog2(n)) 最好情况 O(nLog2(n)) O(n) 稳定

猜你喜欢

转载自blog.csdn.net/hf19931101/article/details/79077874
今日推荐