8个排序算法

Posted beixiaobei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8个排序算法相关的知识,希望对你有一定的参考价值。

 

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/*
1、插入排序。稳定
(1)原理:逐个处理待排序记录,每个记录与前面已排序子序列进行比较,将其插入子序列中正确位置
(2)复杂度:O(n)-》O(n^2),O(n^2)
最佳:升序。时间复杂度为O(n)
最差:降序。时间复杂度为O(n^2)
平均:对于每个元素,前面有一半元素比它大。时间复杂度为O(n^2)

注意:如果待排序数据已经“基本有序”,使用插入排序可以获得接近O(n)的性能
*/
void insertsort(int *data, int n)
{
    if (data == nullptr || n <= 1)
        return;
    for (int i = 1; i < n; i++)
    {
        for (int j = i; j >= 1 && data[j] < data[j - 1]; j--) //升序
            swap(data[j],data[j - 1]);
    }
}
/*插入排序优化:设置哨兵保存待排序值,将比其大的都后移一位,最后插入该值*/
void insertsort_optimization(int *data, int n)
{
    if (data == nullptr || n <= 1)
        return;
    for (int i = 1; i < n; i++)
    {
        int j = i;
        int tmp = data[j];
        for (; j >= 1 && tmp < data[j - 1]; j--) //升序
            data[j] = data[j - 1];
        data[j] = tmp;
    }
}

/*
2、冒泡排序。稳定
(1)原理:从数组的底部比较到顶部,比较相邻元素。
           如果下面的元素更小则交换,否则,上面的元素继续往上比较。
           这个过程每次使最小元素像个“气泡”似地被推到数组的顶部。
(2)复杂度(bubsort_optimization):On-》On^2 ,On^2
*/
void bubsort(int *data, int n)
{
    if (data == nullptr || n <= 1)
        return;
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = n - 1; j > i; j--)
        {
            if (data[j] < data[j - 1])//升序
                swap(data[j], data[j - 1]);//#include <algorithm>
        }
    }
}
/*
冒泡排序优化:增加一个变量flag,用于记录一次循环是否发生了交换,如果没发生交换说明已经有序,可以提前结束
              避免因已经有序的情况下的无意义循环判断
*/
void bubsort_optimization(int *data, int n)
{
    if (data == nullptr || n <= 1)
        return;
    bool flag = true;
    for (int i = 0; i < n - 1 && flag; i++)
    {
        flag = false;
        for (int j = n - 1; j > i; j--)
        {
            if (data[j] < data[j - 1])//升序
            {
                swap(data[j], data[j - 1]);//#include <algorithm>
                flag = true;
            }
        }
    }
}

/*
3、选择排序。不稳定
(1)原理:每次从未排序的序列中找到最小元素,放到未排序数组的最前面
(2)复杂度:O(n^2)
不管数组是否有序,在从未排序的序列中查找最小元素时,都需要遍历完最小序列,所以最差好平均都是O(n^2)

(3)优化:每次内层除了找出一个最小值,同时找出一个最大值(初始为数组结尾)。
将最小值与每次处理的初始位置的元素交换,将最大值与每次处理的末尾位置的元素交换。
这样一次循环可以将数组规模减小2,相比于原有的方案(减小1)会更快
*/
void selectsort(int *data, int n)
{
    if (data == nullptr || n <= 1)
        return;
    for (int i = 0; i < n - 1; i++)
    {
        int lowindex = i;
        for (int j = i + 1; j < n; j++)
        {
            if (data[j] < data[lowindex])
                lowindex = j;
        }
        swap(data[i], data[lowindex]);
    }
}

/*
4、希尔排序。不稳定(插入排序的改进版本)
(1)原理:shell排序在不相邻的元素之间比较和交换。
利用了插入排序的最佳时间代价特性,它试图将待排序序列变成基本有序的,然后再用插入排序来完成排序工作

在执行每一次循环时,Shell排序把序列分为互不相连的子序列,并使各个子序列中的元素在整个数组中的间距相同,
每个子序列用插入排序进行排序。每次循环增量是前一次循环的1/2,子序列元素是前一次循环的2倍
最后一轮将是一次“正常的”插入排序(即对包含所有元素的序列进行插入排序)

(2)复杂度:O(n^(1.5))
选择适当的增量序列可使Shell排序比其他排序法更有效,一般来说,增量每次除以2时并没有多大效果,而“增量每次除以3”时效果更好
当选择“增量每次除以3”递减时,Shell排序的 平均 运行时间是O(n^(1.5))
*/
void shellsort(int *data, int n) 
{
    if (data == nullptr || n <= 1)
        return;
    const int incregap = 3;//增量每次除以3
    /*遍历所有增量大小。incre是增量。每次增量变上次的1/incregap,子序列元素变上次的incregap倍*/
    for (int incre = n / incregap; incre > 0; incre /= incregap)
    {
        for (int i = 0; i < incre; i++)//共有incr个子序列需要分别进行插入排序
        {
            /*对子序列进行插入排序,当增量为1时,对所有元素进行最后一次插入排序*/
            for (int j = i + incre; j < n; j += incre)//每次插入排序都从子序列的第二个值开始,因为认为第一个值已经有序
            {
                for (int k = j; k > i&&data[k] < data[k - incre]; k -= incre)
                    swap(data[k], data[k - incre]);
            }
        }
    }
}
/*
5、快速排序。不稳定。快速排序是所有内部排序算法中平均性能最优的排序算法
(1)原理:首先选择一个轴值,小于轴值的元素被放在数组中轴值左侧,大于轴值的元素被放在数组中轴值右侧,
           这称为数组的一个分割(partition)。快速排序再对轴值左右子数组分别进行类似的操作

(2)复杂度:O(nlogn)-》O(n^2),O(nlogn)
最佳情况:O(nlogn)
最差情况:每次处理将所有元素划分到轴值一侧,O(n^2)
平均情况:O(nlogn)   快速排序平均情况下运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。

(3)优化:
(3.1)最明显的改进之处是轴值的选取,如果轴值选取合适,每次处理可以将元素较均匀的划分到轴值两侧:
三者取中法:三个随机值的中间一个。为了减少随机数生成器产生的延迟,可以选取首中尾三个元素作为随机值
(3.2)当n很小时,快速排序会很慢。因此当子数组小于某个长度(经验值:9)时,此时数组已经基本有序,最后调用一次插入排序完成最后处理
  
(4)选择轴值有多种方法:
最简单的方法是使用首或尾元素。但是,如果输入的数组是正序或者逆序时,会将所有元素分到轴值的一边。
较好的方法是随机选取轴值
*/
int partition(int *data, int start, int end)
{
    //选择尾元素为轴值。较好的方法是随机选取轴值,然后和尾元素交换
    int small = start - 1;//分割点
    for (int index = start; index < end; index++)
    {
        if (data[index] < data[end])//选择尾元素为轴值
        {
            ++small;
            if (small != index)
                swap(data[small], data[index]);
        }
    }
    ++small;
    swap(data[small], data[end]);//将轴值从末尾放到分割点
    return small;//返回分割点。其左小于它,右大于它
}
void qsort(int *data, int start, int end)
{
    if (data == nullptr || end <= start)
        return;
    int index = partition(data, start, end);
    qsort(data, start, index - 1);
    qsort(data, index + 1, end);
}

/*
6、归并排序。稳定。
(1)原理:将一个序列分成两个长度相等的子序列,为每一个子序列排序,然后再将它们合并成一个序列。合并两个子序列的过程称为归并
     二路归并:将n个记录分成长度为1的多个子序列,然后两两归并,得到长度为2的子序列,再两两归并,直到得到长度为n的有序序列为止

(2)复杂度:时间:O(nlogn);空间:O(n)
logn层递归中,每一层都需要O(n)的时间代价,因此总的时间复杂度是O(nlogn),
该时间复杂度不依赖于待排序数组中数值的相对顺序。因此,是最佳,平均和最差情况下的运行时间
由于需要一个和带排序数组大小相同的辅助数组,所以空间代价为O(n)
*/
void mergesortcore(int *data, int *temp, int i, int j)
{
    if (i == j) return;
    int mid = (i + j) / 2;
    mergesortcore(data, temp, i, mid);
    mergesortcore(data, temp, mid + 1, j);

    /*二路归并*/
    //data中是两个分别已排序好的子序列,两个归并排序后放入额外空间temp,再用temp更新对应位置的data
    int index1 = i, index2 = mid + 1, current = i;//二路子序列:index1-mid  index2-j(end)
    while (index1 <= mid && index2 <= j)
    {
        if (data[index1] < data[index2])
            temp[current++] = data[index1++];
        else
            temp[current++] = data[index2++];
    }
    while (index1 <= mid)
        temp[current++] = data[index1++];
    while (index2 <= j)
        temp[current++] = data[index2++];
    for (current = i; current <= j; current++)
        data[current] = temp[current];
}
void mergesort(int *data, int size)
{
    if (data == nullptr || size <= 1)
        return;
    int *temp = new int[size]();//new[] 数组,()初始化为0
    mergesortcore(data, temp, 0, size - 1);
    delete[] temp;
}

/*
归并排序优化空间(原地归并排序):O1空间,O(nlogn)时间。
不用额外空间,两个有序子序列归并时,如果后一个子序列的值小,则前一个子序列全部后移一位,将后一个子序列的小值放到前一个子序列前

【arking原链接测试数据交换2和9就错误,即mid大于左半部分就错误。但其交换两部分内容的方法很好:分别翻转左右部分,再整体翻转】
*/
void mergesort_optimization_core(int *data, int start, int end)
{
    if (start == end)
        return;
    int mid = (start + end) / 2;
    mergesort_optimization_core(data, start, mid);
    mergesort_optimization_core(data, mid + 1, end);

    int index1 = start, index2 = mid + 1;
    while (index1 <= mid && index2 <= end)//二路子序列:index1-mid;index2-end
    {
        if (data[index1] > data[index2])//移动第一个子序列:从index1到mid全部后移一位,把原index2处内容放到index1处。更新mid和end
        {
            int index2_tmp = data[index2];
            for (int t = index2; t > index1; t--)
                data[t] = data[t - 1];
            data[index1] = index2_tmp;
            ++mid;
            ++index2;
        }
        ++index1;//无论是否移动第一个子序列都要更新index1
    }
}
void mergesort_optimization(int *data, int size)
{
    if (data == nullptr || size <= 1)
        return;
    mergesort_optimization_core(data,0,size-1);
}

/*
完全二叉树:假设一个二叉树有n层,那么如果第1到n-1层的每个节点都达到最大的个数:2,
            且第n层的排列是从左往右依次排开的,那么就称其为完全二叉树
堆概念:本身是一个完全二叉树,当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆;
        当二叉树的每个节点都小于它的子节点的时候,称为小顶堆。
        (stl的make_heap默认大顶堆,使用函数对象less<int>(),而一般sort排序默认此函数对象意味着升序)
堆性质:将堆的内容从左往右,从上至下层次遍历放入数组,
        若一个结点在数组中下标为k,那么它的父结点为(k-1)/2,其子节点为2k+1和2k+2

7、堆排序。不稳定。
(1)原理:首先根据数组构建最大堆,然后每次“删除”堆顶元素(将堆顶元素移至末尾),并调整剩余元素为最大堆。
           最后得到的序列就是从小到大排序的序列。

(2)复杂度:Onlogn
           (根据已有数组构建堆需要O(n)的时间复杂度,每次删除堆顶并调整堆需要O(logn)的时间复杂度,对数组n的排序需要取n-1次堆顶记录
            所以总的时间开销为,O(n+nlogn),平均时间复杂度为O(nlogn)

(3)应用:
(3.1)根据已有元素建堆是很快的,如果希望找到数组中第k大的元素,可以用O(n+klogn)的时间,如果k很小,时间开销接近O(n)
(3.2)从10亿个浮点数当中,选出其中最大的10000个:大数据不适宜载入内存,可使用外排,效率低;也可构建topk的最小堆,每次和根比较,替换根后调整堆
*/
void heapsortcore(vector<int> &data, int parent, int size)
{
    int leftchild = 2 * parent + 1;
    if (leftchild < size)//确保有左孩子,否则data[xx]会越界访问
    {
        int rightchild = leftchild + 1;
        int maxchild = leftchild;//左右孩子中大的那个孩子下标    

        //存在右孩子,则需取得左右孩子中大的那个节点,与父节点比较,把最大的放到父节点,
        //如果做了交换,则需继续递归调整后续子树
        if (rightchild < size)
        {
            if (data[leftchild] < data[rightchild])
                maxchild = rightchild;
        }
        if (data[parent] < data[maxchild])
        {
            swap(data[parent], data[maxchild]);
            heapsortcore(data, maxchild, size);//做了交换,继续递归调整后续子树
        }
    }
}
void heapsort(vector<int> &data)
{
    if (data.size() <= 1)
        return;

    //从无序数组开始建堆:从最后一个叶子节点的父节点开始
    for (int i = (data.size() - 2) / 2; i >= 0; i--)
    {
        heapsortcore(data, i, data.size());//从(size - 2) / 2开始
    }

    //循环将 根节点放到尾部,对除尾部外其余元素重新调整为堆 即可调整数组为有序数组
    //(调整则需只从root开始,和其子节点比较并交换,调整后续子树
    for (int j = data.size() - 1; j > 0; j--)
    {
        swap(data[0], data[j]);
        heapsortcore(data, 0, j);//实际是做了交换,继续递归调整后续子树(和插入到头部做调整不同,插入会改变树结构,可能需要调整全部,而此交换树结构不变,还可重用之前结果,只需调整对应子树)
    }
}

/*针对已经构建好的堆,插入一个元素到尾部,再和parent比较调整为新的堆,只需调整子树
(如果插入到头部,后续节点数组坐标改变,有别的本无须调整的分支可能变成不是最小堆,就需要和开始一样从新建堆,复杂度On>Olog)*/
void insert_heapsort(vector<int> &data, int val)
{
    //从无序数组开始建堆:从最后一个叶子节点的父节点开始
    for (int i = (data.size() - 2) / 2; i >= 0; i--)
    {
        heapsortcore(data, i, data.size());//从(size - 2) / 2开始
    }
    //针对已经构建好的堆,插入一个元素到尾部,再调整为新的堆,只需调整子树
    data.push_back(val);
    int valindex = data.size() - 1;//从插入元素往前调整
    int parent = (valindex - 1) / 2;
    while (valindex > 0)
    {
        if (data[parent] < data[valindex])
        {
            swap(data[parent], data[valindex]);
            valindex = parent;//为继续调整子树做准备
            parent = (valindex - 1) / 2;
        }
        else
            break;//调整到已经比parent小了,由于本就是一个最大堆,只是做调整子树,此处表明所有父节点都比它大,直接break
    }
    //堆排序输出
    for (int j = data.size() - 1; j > 0; j--)
    {
        swap(data[0], data[j]);
        heapsortcore(data, 0, j);//实际是做了交换,继续递归调整后续子树
    }
}
/*
STL的堆排序sort_heap(vi.begin(), vi.end());
循环使用pop_heap实现
*/
void stl_heapsort(vector<int> &data)
{
    if (data.size() <= 1)
        return;
    //int end = data.size();
    make_heap(data.begin(), data.end());//最大堆
    for (int i = 0; i < data.size(); i++)
    {
        pop_heap(data.begin(), data.end()-i);//将root元素移到尾部,再对其余元素重新调整堆
        //--end;
    }
}

/*
8、基数排序。稳定。
(1)原理:桶排序扩展。将整数按位数切割成不同的数字,然后按每个位数分别比较。数位较短的数前面补零
           低位是有序的,高位中如果有相同的值,则只需在保持稳定的前提下对高位进行排序,结果自然有序。
(2)复杂度:时间Od(n+k),空间On+k  k为基数
空间On+k,k为基数10
时间:需要进行最大数位数d次分配和收集,一趟分配On,一趟收集Ok,总共Od(n+k)
*/
void radixsortcore(vector<int> &data, int exp)
{
    //data[i]从后往前数的第exp位值(data[i]/exp)%10  为下标的元素个数
    vector<int> valuecount(10, 0);

    for (int i = 0; i < valuecount.size(); i++)//对于较短的数,高位此处计算为0,则后续会排到前面,且相对位置不变
        valuecount[(data[i] / exp) % 10]++;
    
    //valuecount表示data该出现在排序数组中的下标。valuecount[i]表示i之前的数据出现次数,也即当前数本应该在的位置。
    //如果有3个数个位为1,2个数个位为0,则个位为1的3个数下标从3+2-1开始到3+0-1
    for (int i = 1; i < 10; i++)
        valuecount[i] += valuecount[i - 1];

    //从数组尾部开始放入排序数组对应位置(根据上面求出的每个值的个数),确保相对位置稳定
    vector<int> tmpdata(data.size(), 0);
    
    for (int i = data.size() - 1; i >= 0; i--)
    {
        tmpdata[valuecount[(data[i] / exp) % 10] - 1] = data[i];
        --valuecount[(data[i] / exp) % 10];
    }
    data = tmpdata;
}
void radixsort(vector<int> &data)
{
    if (data.size() <= 1)
        return;
    int max = -1;
    for (int i = 0; i < data.size(); i++)
    {//找出最大值
        if (data[i] > max)
            max = data[i];
    }
    for (int exp = 1; max / exp > 0; exp *= 10)//exp从个位到十位到...
        radixsortcore(data, exp);
}



int main() {
#if 0
    //int data[] = { 0,1,5,6,2,9,30,4,7,8 };

    //insertsort(data, 10);//插入排序。稳定。  时间On-》On^2,On^2
    //insertsort_optimization(data, 10);//插入排序优化。设置哨兵,减少交换
    
    //bubsort(data, 10);//冒泡排序。稳定。  时间On-》On^2 ,On^2
    //bubsort_optimization(data, 10);//冒泡排序优化。增加flag,提前结束
    
    //selectsort(data, 10);//选择排序。不稳定。时间O(n^2)
    
    //shellsort(data,10);//希尔排序(在插入排序的基础上)。不稳定。时间On^1.5
    
    //qsort(data, 0, 9);//快速排序。不稳定。时间Onlogn-》On^2,Onlogn

    //mergesort(data, 10);//归并排序。稳定。Onlogn时间,On空间
    //mergesort_optimization(data, 10);//归并排序优化。Onlogn时间,O1空间
    
    for (int i = 0; i < sizeof(data) / sizeof(int); i++)
        cout << data[i] << " ";
    cout << endl;
#endif

//#if 0
    vector<int> data(10,0);
    for (int i = 0; i < 10; i++)
        data[i] = 9-i;
    cout << "排序前: ";
    for (vector<int>::iterator it = data.begin(); it != data.end(); it++)
        cout <<*it << " ";
    cout << endl;

    
    heapsort(data);//堆排序。不稳定。Onlogn
    //stl_heapsort(data);//stl堆排序
    //int val = 10;
    //insert_heapsort(data,val);//插入元素到已经排序好的堆,重新调整为堆,再排序输出

    //radixsort(data);//基数排序,针对非负整数。稳定。时间Od(n+k),空间On+k  k为基数
    cout << "排序后: ";
    for (vector<int>::iterator it = data.begin(); it != data.end(); it++)
        cout << *it << " ";
    cout << endl;

//#endif
    return 0;
}

 

以上是关于8个排序算法的主要内容,如果未能解决你的问题,请参考以下文章

8个常用算法的超常剖析

快速排序-递归实现

8个常用的排序算法

在第6731次释放指针后双重免费或损坏

8个排序算法

经典排序算法——折半插入排序