首页 > 技术文章 > 【算法】常用排序算法总结(C++ )

nntzhc 2020-12-24 22:11 原文

 

1 快速排序

快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

稳定性问题 

    首先大家应该都知道快速排序是一个不稳定排序算法,那么到底什么才是排序的稳定性呢,我认为通俗的讲有两个相同的数A和B,在排序之前A在B的前面,而经过排序之后,B跑到了A的前面,对于这种情况的发生,我们管他叫做排序的不稳定性,而快速排序在对存在相同数进行排序时就有可能发生这种情况。

    例如(5,3A,6,3B)对这个进行排序,排序之前相同的数3A与3B,A在B的前面,经过排序之后会变成

        (3B,3A,5,6),所以说快速排序是一个不稳定的排序

时间复杂度    

     最优情况:每一次的flag刚好都可以平分整个数组,此时的时间复杂度为O(nlogn)

     最坏情况:每一次的flag刚好都是最大或者最小的数,此时的时间复杂度为O(n2)

     平均情况:平均情况为O(nlogn),接近最优情况。

//递归写法
using
namespace std; int partition(vector<int> &nums, int left, int right) { int mid = (left + right) >> 1; swap(nums[mid], nums[right]); int key = nums[right]; int last_small_index = left -1; for (int i = left; i < right; ++i) { if (nums[i] < key) { ++last_small_index; if (i > last_small_index) swap(nums[last_small_index], nums[i]); } } swap(nums[++last_small_index], nums[right]); return last_small_index; } void quick_sort(vector<int> &nums,int left,int right) { if (right == left) return; int div = partition(nums, left, right); if(div > left) quick_sort(nums, left, div-1); if (div < right) quick_sort(nums, div+1, right); } int main() { vector<int> nums = { 3,14,1,5,0,2,4,6,5}; int left = 0; int right = nums.size() - 1; quick_sort(nums, left, right); for (int i = 0; i <= right; ++i) { cout << nums[i]<<" "; } }

 

//非递归写法(迭代+栈)
void QuickSort_Iteration(vector<int>& nums)    
{
    if (nums.empty())
    {
        return;
    }
    stack<int> sta;
    sta.push(0);
    sta.push(nums.size() - 1);
    int le, ri, mi;
    while (!sta.empty())
    {
        ri = sta.top();  
        sta.pop();
        le = sta.top();
        sta.pop();
        mi = partition(nums, le, ri);
        if (mi - 1 > le)
        {
            sta.push(le);
            sta.push(mi - 1);
        }
        if (mi + 1 < ri)
        {
            sta.push(mi + 1);
            sta.push(ri);
        }
    }
}

 

2 归并排序

“归并”一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/21([x1表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,…,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。


//递归写法

using
namespace std; void merge_sort(vector<int> &nums,int left,int right,vector<int> &ret) { if (right - left <= 1) return; int mid = (left + right) >> 1; merge_sort(nums, left, mid, ret); merge_sort(nums, mid, right, ret); int index1 = left, index2 = mid, index = left; while(index1<mid||index2<right) { if (index2 >= right || (nums[index1] <= nums[index2] && index1 < mid)) ret[index++] = nums[index1++]; else ret[index++] = nums[index2++]; } for (int i = left; i < right; ++i) { nums[i] = ret[i]; } } int main() { vector<int> nums = { 3,14,1,5,0,2,4,6,5}; vector<int> ret(nums.size()); merge_sort(nums, 0, nums.size(), ret); for (int i = 0; i < nums.size(); ++i) { cout << nums[i]<<" "; } }

 

//非递归写法
void MergeSort_Iteration(vector<int>& nums)
{
    if (nums.empty())
    {
        return;
    }
    int len = nums.size();
    vector<int> temp(len, 0);    //临时数组,下面会用
    for (int merge_step = 1; merge_step < len; merge_step <<= 1)//当merge_step为k,表示merge两个长度k的有序序列成一个长度2k的序列
    {
        for (int begin = 0; begin < len - merge_step; begin += (merge_step << 1))
        {
            int le = begin, ri = begin + merge_step;   //左右区间起点
            int le_end = ri, ri_end = min(ri + merge_step, len);    //左右区间终点(半闭半开区间)
            int i = 0;
            while (le < le_end and ri < ri_end)
            {
                if (nums[le] < nums[ri])
                {
                    temp[i] = nums[le];
                    ++le;
                }
                else
                {
                    temp[i] = nums[ri];
                    ++ri;
                }
                ++i;
            }
            while (le < le_end)
            {
                temp[i] = nums[le];
                ++i;
                ++le;
            }
            while (ri < ri_end)
            {
                temp[i] = nums[ri];
                ++i;
                ++ri;
            }
            for (int j = 0; j < i; ++j)    //写回原数组
            {
                nums[begin + j] = temp[j];
            }
        }
    }
}

 

3 堆排序

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

堆排序的过程:

a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

#include <iostream>
#include <algorithm>
using namespace std;

void max_heapify(int arr[], int start, int end) {
    // 建立父節點指標和子節點指標
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) { // 若子節點指標在範圍內才做比較
        if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的
            son++;
        if (arr[dad] > arr[son]) // 如果父節點大於子節點代表調整完畢,直接跳出函數
            return;
        else { // 否則交換父子內容再繼續子節點和孫節點比較
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    // 初始化,i從最後一個父節點開始調整
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    // 先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}

int main() {
    int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    return 0;
}

 

4 冒泡排序

冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

using namespace std;
void bubble_sort(vector<int> &nums)
{
    bool flag=true;//交换标志位,如果没有发生交换,说明后面已经全部排好序,即可停止
    int length = nums.size();
    for (int i = 0; i < length; ++i)
    {
        for (int j = length - 1; j > i; --j)
        {
            if (nums[j] < nums[j - 1])
            {
                flag = false;
                swap(nums[j], nums[j - 1]);
            }
        }
        if (flag) break;
    }
}

int main() {
    vector<int> nums = { 3,14,1,5,0,2,4,6,5};
    bubble_sort(nums);

    for (int i = 0; i < nums.size(); ++i)
    {
        cout << nums[i]<<"  ";
    }

}

 

5 插入排序

直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

using namespace std;
void insert_sort(vector<int> &nums)
{
    int length = nums.size();
    for (int i = 0; i < length; ++i)
    {
        for (int j = i; j > 0; --j)
        {
            if (nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
        }
    }
}

int main() {
    vector<int> nums = { 3,14,1,5,0,2,4,6,5};
    insert_sort(nums);

    for (int i = 0; i < nums.size(); ++i)
    {
        cout << nums[i]<<"  ";
    }

}

 6 有限种类元素注意优先桶排序

总结(大话数据结构p456)

 

 

从算法的简单性来看,我们将7种算法分为两类:
简单算法:冒泡、简单选择、直接插入。

改进算法:希尔、堆、归并、快速。


从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑4种复杂的改进算法。
从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。

从这三组时间复杂度的数据对比中,我们可以得出这样一个认识。堆排序和归并排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化的天才,心情好时表现极佳,碰到较糟糕环境会变得差强人意。但是他们如果都来比赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是0(1)。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。
从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排序是个好算法。

从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。
反之,n越大,采用改进排序方法越合适。这也就是我们为什么对快速排序优化时,增加了一个阀值,低于阀值时换作直接插入排序的原因。

从表中数据看,似乎简单选择排序在3种简单排序中性能最差,其实也不完全是,比如,如果记录的关键字本身信息量比较大(例如,关键字都是数十位的数字),此时表明其占用存储空间很大,这样移动记录所花费的时间也就越多,我们给出3种简单排序算法的移动次数比较,如表所示。

你会发现,此时简单选择排序就变得非常有优势,原因也就在于,它是通过大量比较后选择明确记录进行移动,有的放矢。因此对于数据量不是很大而记录的关键字信息量较大的排序要求,简单排序算法是占优的。另外,记录的关键字信息量大小对那四个改进算法影响不大。

总之,从综合各项指标来说,经过优化的快速排序是性能最好的排序算法,但是不同的场合我们也应该考虑使用不同的算法来应对它。

 

推荐阅读