代码题(32)— 排序算法总结

 0、几种排序对比

  1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。

  2. 上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。

一、交换排序

1、冒泡排序

  思想:对待排序元素的关键字从后往前进行多遍扫描,遇到相邻两个关键字次序与排序规则不符时,就将这两个元素进行交换。这样关键字较小的那个元素就像一个泡泡一样,从最后面冒到最前面来。
  时间复杂度:最坏:O(n2); 最好: O(n); 平均: O(n2)
  空间复杂度:O(1)
  稳定性稳定,相邻的关键字两两比较,如果相等则不交换。所以排序前后的相等数字相对位置不变。

void BubbleSort(vector<int> &a, int num)
{
    bool flag = true;
    for (int i = 0; i < num ; ++i)
    {
        if (!flag)//判断上一次循环有没有交换
            break;

        flag = false;
        for (int j = num - 1; j > i; --j)
        {
            if (a[j] < a[j - 1])
            {
                swap(a[j], a[j - 1]);
                flag = true;
            }
        }
    }
}

2、快速排序

  思想:该算法是分治算法,首先选择一个基准元素,根据基准元素将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。基准元素的选择对快速排序的性能影响很大,所有一般会想打乱排序数组选择第一个元素或则随机地从后面选择一个元素替换第一个元素作为基准元素。

  总结快速排序的思想:冒泡+二分+递归分治。
  时间复杂度:最坏(正序或逆序):O(n2) ;最好: O(nlogn) ; 平均: O(nlogn)
  空间复杂度:用于方法栈,递归树,最好:O(logn);最坏:O(n);平均:O(logn)
  稳定性不稳定 ,快排会将大于等于基准元素的关键词放在基准元素右边,加入数组 1 2 2 3 4 5 选择第二个2 作为基准元素,那么排序后 第一个2跑到了后面,相对位置发生变化。

//交换数值
int partition_1(vector<int> &a, int low, int high)
{
    int pivot = a[low];
    while (low < high)//直到找到正确的位置
    {
        while (low<high && a[high]>pivot)
            high--;
        swap(a[low], a[high]);
        while (low < high && a[low] <= pivot)
            low++;
        swap(a[low], a[high]);
    }
    return low;
}

//用替换取代交换,提高效率。
int partition(vector<int> &a, int low, int high)
{
    int pivot = a[low];
    while (low < high)//直到找到正确的位置
    {
        while (low<high && a[high]>pivot)
            high--;
        a[low] = a[high];
        while (low < high && a[low] <= pivot)
            low++;
        a[high] = a[low];
        
    }
    a[low] = pivot;//将中轴替换回去
    return low;
}

void qSort(vector<int> &a, int low, int high)
{
    //可以通过判断,当待排序数组大于一定值(50)时就快速排序,
    //否则就采用直接插入排序 (简单排序中性能最好的)。
    int p = 0;
    if (low < high)
    {
        p = partition(a, low, high);
        qSort(a, low, p - 1);
        qSort(a, p + 1, high);
    }
}

void quickSort(vector<int> &a, int num)
{
    if (a.empty() || num <= 0)
        return;
    qSort(a, 0, num - 1);
} 

二、选择排序

3、简单选择排序

  思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后每次从剩余未排序元素中继续寻找最小(大)元素放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
  时间复杂度:最坏:O(n2) 最好: O(n2) 平均: O(n2)
  空间复杂度:O(1)
  稳定性不稳定。 例如数组 2 2 1 3 第一次选择的时候把第一个2与1交换使得两个2的相对次序发生了改变。

void SelectSort(vector<int> &a, int num)
{
    for (int i = 0; i < num; ++i)
    {
        int min = i;
        for (int j = i + 1; j < num; ++j)// 每次循环找后面数组的最小值位置
        {
            if (a[j] < a[min])
                min = j;
        }
        if (min != i)
            swap(a[i], a[min]);
    }
}

4、堆排序

  思想:堆排序是利用堆的性质进行的一种选择排序,先将排序元素构建一个最大堆,每次堆中取出最大的元素并调整堆。将该取出的最大元素放到已排好序的序列前面。这种方法相对选择排序,时间复杂度更低,效率更高。
  时间复杂度:最坏:O(nlogn); 最好: O(nlogn) ;平均: O(nlogn)
  空间复杂度:O(1)
  稳定性不稳定 ,例如 5 10 15 10。 如果堆顶5先输出,则第三层的10(最后一个10)的跑到堆顶,然后堆稳定,继续输出堆顶,则刚才那个10跑到前面了,所以两个10排序前后的次序发生改变。不适用于排序个数较少的情况。

void heapAdjust(vector<int> &a, int k, int num)
{
    int temp = a[k];
    for (int i = 2 * k; i < num; i *= 2) //循环以k结点为根节点的子树中的孩子
    {
        if (i < num - 1 && a[i] < a[i + 1])//找到左、右孩子中较大值的下标,判断是否满足条件
            i = i + 1;
        if (a[i] <= temp) //如果根比左右孩子都大,则不交换
            break;
        a[k] = a[i]; //将较大值放到根节点上
        k = i;  // 换标记的下标,k始终指向原来的数的位置
    }
    a[k] = temp; //插入该值
}

void heapSort(vector<int> &a, int num)
{
    // 将输入数组构建成一个大根堆,找到有孩子的结点,自下向上,自右向左,将以该结点的子树构建大根堆
    for (int i = num / 2; i >=0; --i)
    {
        heapAdjust(a, i, num);
    }
    
    for (int i = num - 1; i > 0; --i)
    {
        swap(a[0], a[i]);
        heapAdjust(a, 0, i); //将调整后剩下的数调整为大根堆
    }
}

 三、插入排序

5、直接插入排序

  思想:每次将一个待排序的数据按照其关键字的大小插入到前面已经排序好的数据中的适当位置,直到全部数据排序完成。
  时间复杂度:最坏:O(n2) ;最好:O(n) ;平均:O(n2) 
  空间复杂度:O(1)
  稳定性稳定, 每次都是在前面已排好序的序列中找到适当的位置,只有小的数字会往前插入,所以原来相同的两个数字在排序后相对位置不变。

void InsertSort(vector<int> &a, int num)
{
    int j, temp;
    for (int i = 1; i < num; ++i)
    {
        temp = a[i];
        j = i - 1;
        while (j >= 0 && a[j] > temp)//找到插入位置
        {
            a[j+1] = a[j];
            j--;
        }
        a[j+1] = temp;//注意这里是 j+1
    }
}

6、希尔排序

  思想:希尔排序根据增量值对数据按下表进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整体采用直接插入排序得到有序数组,算法终止。
  时间复杂度:最坏:O(n2) ;最好:O(n1.3) ;平均:O(n) 
  空间复杂度:O(1)
  稳定性不稳定, 因为是分组进行直接插入排序,原来相同的两个数字可能会被分到不同的组去,可能会使得后面的数字会排到前面,使得两个相同的数字排序前后位置发生变化。
  不稳定举例: 4 3 3 2 按2为增量分组,则第二个3会跑到前面

void shellSort(vector<int> &a, int num)
{
    int k, temp;
    for (int gap = num / 2; gap > 0; gap /= 2)
    {
        for (int i = 0; i < num - gap; ++i)//每隔一定距离的数组进行 插入排序
        {
            if (a[i] > a[i + gap])
            {
                temp = a[i + gap];
                k = i;
                while (k >= 0 && a[k] > temp)//插入排序,对应数字向后移动
                {
                    a[k + gap] = a[k];
                    k -= gap;
                }
                a[k + gap] = temp;
            }
        }
    }
}

7、归并排序

  思想:归并排序采用了分治算法,首先递归将原始数组划分为若干子数组,对每个子数组进行排序。然后将排好序的子数组递归合并成一个有序的数组。
  时间复杂度:最坏:O(nlogn) 最好: O(nlogn) 平均: O(nlogn)
  空间复杂度:O(n)
  稳定性:稳定

(1)递归方法


void
merge(vector<int> &a, int left, int mid, int right) { vector<int> temp(right-left+1);//创建临时数组存排序好的 int i = left; int j = mid+1; int k = 0; while (i <= mid && j <= right) { if (a[i] < a[j]) temp[k++] = a[i++]; else temp[k++] = a[j++]; } //处理剩余未合并的部分 while (i <= mid) temp[k++] = a[i++]; while (j <= right) temp[k++] = a[j++]; //将临时数组中的内容存储到原数组中 for (int p = 0; p < temp.size(); ++p) a[left+p] = temp[p]; } void mSort(vector<int> &a, int left, int right) { if (left >= right) return; int mid = (left + right) / 2; mSort(a, left, mid);//递归左边 mSort(a, mid + 1, right);//递归右边 merge(a, left, mid, right);//合并 } void mergeSort(vector<int> &a, int num) { mSort(a, 0, num - 1); }

8、基数排序算法

  思想:基数排序是通过“分配”和“收集”过程来实现排序,首先根据数字的个位的数将数字放入0-9号桶中,然后将所有桶中所盛数据按照桶号由小到大,桶中由顶至底依次重新收集串起来,得到新的元素序列。然后递归对十位、百位这些高位采用同样的方式分配收集,直到没各位都完成分配收集得到一个有序的元素序列。
  时间复杂度:最坏:O(d(r+n)) 最好:O(d(r+n)) 平均: O(d(r+n))
  空间复杂度:O(dr+n) n个记录,d个关键码,关键码的取值范围为r
  稳定性稳定 ,基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

9、桶排序

  基本思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法(快排)或是以递回方式继续使用桶排序进行排序)。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
         简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。

  时间复杂度最坏:O(d(r+n)) 最好:O(n) 平均: O(d(r+n))
  空间复杂度
O(m+n) ,如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的
  稳定性:稳定
 。

  但桶排序的缺点是:

        1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。

        2)其次待排序的元素都要在一定的范围内等等。

  算法步骤举例:

  例如要对大小为[1..1000]范围内的n个整数A[1..n]排序  。

  1.   首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储   (10..20]的整数,……集合B[i]存储(   (i-1)*10,   i*10]的整数,i   =   1,2,..100。总共有  100个桶。 
  2.   然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 
  3.   再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任  何排序法都可以。
  4.   最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这  样就得到所有数字排好序的一个序列了。  

  代码仅供参考:

void radix(int* a, int len ) {//times指最大值的位数
    int i, j, temp, l, base = 1, t, index;
    int times = 2;
    for (t = 1; t <= times; t++) { //个位、十位、百位、千位...
                                   //内部使用的稳定排序为桶排序
        vector<int> v[10];//桶[0][1][2][3][4][5][6][7][8][9]
        for (i = 0; i < len; i++) {
            l = a[i] / base % 10;//取出当前位数的值,对应桶的位置
            v[l].push_back(a[i]);
            if (v[l].size() > 1) {
                for (j = v[l].size() - 1; j > 0; j--) {
                    if (v[l][j] >= v[l][j - 1]) break; //等号可保证稳定排序,很重要
                    temp = v[l][j];
                    v[l][j] = v[l][j - 1];
                    v[l][j - 1] = temp;
                }
            }
        }
        index = 0;
        for (i = 0; i < 10; i++) {
            for (j = 0; j < v[i].size(); j++) {
                a[index++] = v[i][j];
            }
        }
        base *= 10;
    }
}

参考文献:https://www.cnblogs.com/jaylon/p/5735431.html

  https://www.cnblogs.com/jiangyang/p/5466329.html

原文地址:https://www.cnblogs.com/eilearn/p/9406532.html