常用排序算法总结(插入、冒泡、选择、希尔、快速、归并、堆)


简述

排序算法的稳定性:如果 Ai=Aj ,排序前后 Ai Aj 的相对位置不变,则称这种排序算法是稳定的,反之,则是不稳定的。

排序算法稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。例如基数排序,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。

这里写图片描述


插入排序

  • 直接插入
  • 二分插入

(1)直接插入:类似于扑克牌插入,对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。

这里写图片描述

空间复杂度:在实现上,需用到 O(1) 的额外空间,因为在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
// 分类 ---------- 内部比较排序
// 数据结构 ------- 数组
// 最差时间复杂度 -- 输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 -- 输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 -- O(n^2)
// 所需辅助空间 --- O(1)
// 稳定性 -------- 稳定
int *insertSort(int *array, int num)
{
    int i, j, target;
    for(i = 1; i < num; i++)
    {
        target = array[i];// 右手抓到一张扑克牌
        j = i;
        while(target < array[j-1] && j >= 1)// 将抓到的牌与手牌从右向左进行比较
        {
            array[j] = array[j-1];// 如果该手牌比抓到的牌大,就将其右移
            j--;
        }
        //直到该手牌比抓到的牌小或相等,将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
        array[j] = target;
    }
    return array; 
 }

注意:插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

(2)二分插入:如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,称为二分插入排序。

int *insertSort(int *array, int num)
{
    int i, j, target;
    int left, right, middle;
    for(i = 1; i < num; i++)
    {
        target = array[i];// 右手抓到一张扑克牌
        left = 0;
        right = i - 1;
        while(left <= right)//二分查找过程
        {
            middle = (left + right) / 2;
            if(array[middle] > target)
                right = middle - 1;
            else
                left = middle + 1;
        }
        for(j = i - 1; j >= left; j--)// 右移
        {
            array[j] = array[j-1];
        }
        array[left] = target;
    }
    return array; 
 }

冒泡排序

基本思想:依次比较相邻的两个元数,将小的数放在前面,大的数放在后面。

具体算法描述如下:

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 原始数据有序,在内部循环中使用一个标识来表示有无交换操作,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
int *bubble(int *array, int num)
{
    int i, j, temp;
    for(i = 0; i < num; i++)
    {
        for(j = 0; j < num-i-1; j++)
        {
            if(array[j+1] < array[j])
            {
                temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
    }
    return array
 }

冒泡排序的改进:鸡尾酒排序:也叫定向冒泡排序。冒泡排序则仅从低到高去比较序列里的每个元素,而鸡尾酒排序是从低到高然后从高到低去比较序列中的每个元素。可以得到比冒泡排序稍微好一点的效能。

#include <stdio.h>

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void exchange(int A[], int i, int j)        // 交换A[i]和A[j]
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

int main()
{
    int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };   // 从小到大定向冒泡排序
    int n = sizeof(A) / sizeof(int);                
    int left = 0;                           // 初始化边界
    int right = n - 1;
    while (left < right)
    {
        for (int i = left; i < right; i++)  // 前半轮,将最大元素放到后面
            if (A[i] > A[i + 1]) 
            {
                exchange(A, i, i + 1);
            }
        right--;
        for (int i = right; i > left; i--)  // 后半轮,将最小元素放到前面
            if (A[i - 1] > A[i]) 
            {
                exchange(A, i - 1, i);
            }
        left++;
    }
    return 0;
}

选择排序

基本思想:遍历列表,并且将最小的元素与第一个元素交换;再次遍历剩余的元素并将次小的元素与第二个元素交换,依次类推。

选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
int *selectSort(int *array, int n)
{
    int smallIndex, i, j, temp; 
    for (i= 0; i< n - 1; i++)
    {
        // 假定最小值初始时是arr[i]=1st
        smallIndex = i;
        // 遍历子列表,从arr[i+1]到arr[n-1],因为从0到i已经是有序的了
        for (j = i+ 1; j < n; j++)
        {
            //如果发现小元素,把当前元素的索引赋值给最小值
            if (arr[j] < arr[smallIndex])
            {
                smallIndex = j;
            }  
        }
        //交换
        temp = arr[i];
        arr[i] = arr[smallIndex];
        arr[smallIndex] = temp;
    }
}

注意:插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

(2)二分插入:如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目,称为二分插入排序。

int *insertSort(int *array, int num)
{
    int i, j, target;
    int left, right, middle;
    for(i = 1; i < num; i++)
    {
        target = array[i];// 右手抓到一张扑克牌
        left = 0;
        right = i - 1;
        while(left <= right)//二分查找过程
        {
            middle = (left + right) / 2;
            if(array[middle] > target)
                right = middle - 1;
            else
                left = middle + 1;
        }
        for(j = i - 1; j >= left; j--)// 右移
        {
            array[j] = array[j-1];
        }
        array[left] = target;
    }
    return array; 
 }

希尔排序

也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

基本思想:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

具体算法描述如下:
以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例

第一次 gap = 10 / 2 = 5,即分成了5组(49, 13) (38, 27) (65, 49) (97, 55) (26, 4),这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26)

第二次 gap = 5 / 2 = 2,即分成了2组(13,49,4,38,97)(27,55,49,65,26),排序后变成了(4,13,38,49,97)(26,27,49,55,65)

第三次 gap = 2 / 2 = 1,即分成了1组,直接使用插入排序,最后得到(4 13 26 27 38 49 49 55 65 97)

第四次 gap = 1 / 2 = 0 排序完成

void shellsort1(int a[], int n)  
{  
    int i, j, gap;  

    for (gap = n / 2; gap > 0; gap /= 2) //步长  
        for (i = 0; i < gap; i++)        //直接插入排序  
        {  
            for (j = i + gap; j < n; j += gap)   
                if (a[j] < a[j - gap])  
                {  
                    int temp = a[j];  
                    int k = j - gap;  
                    while (k >= 0 && a[k] > temp)  
                    {  
                        a[k + gap] = a[k];  
                        k -= gap;  
                    }  
                    a[k + gap] = temp;  
                }  
        }  
}

对上面的代码进行简化

void shellsort3(int a[], int n)  
{  
    int i, j, gap;  

    for (gap = n / 2; gap > 0; gap /= 2)  
        for (i = gap; i < n; i++)  //从数组第gap个元素开始
            for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)  //每个元素与自己组内的数据进行直接插入排序
                Swap(a[j], a[j + gap]);  
}  

快速排序

基本思想:挖坑填数+分治法
1. 先从数列中取出一个数作为基准数。
2. 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3. 再对左右区间重复第二步,直到各区间只有一个数。

具体算法描述如下:

例如无序数组[6 2 4 1 5 9]

a. 先把第一项[6]取出来,用[6]依次与其余项进行比较,如果比[6]小就放[6]前边,否则就放[6]后边。排序前 6 2 4 1 5 9,排序后 5 2 4 1 6 9

b. 对前半拉[5 2 4 1]继续进行快速排序,重复步骤a后变成下边这样:
排序前 5 2 4 1,排序后 1 2 4 5
前半拉排序完成,总的排序也完成。

// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大的元素(或者每次都是最小),导致每次只划分出了一个子序列,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都能使划分均匀,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(logn)~O(n),主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)(基本有序的情况)
// 稳定性 ---------- 不稳定
int patition(int *array, int low, int high)
{
    int temp = array[low];
    while(low<high)
    {
        while(low < high && array[high] > temp)     high--;
        array[low] = array[high];
        while(low < high && array[low] <= temp)     low++;
        array[high] = array[low];
        for(int i = 0; i < high+1; i++)
        {
            printf("%d\t",array[i]);
        }
        printf("\n");
    }
    array[low] = temp;
    return low;
}

int *quick(int *array, int low, int high)
{
    int mid = 0;
    if(low < high)
    {
        mid = patition(array,low,high);
        quick(array,low,mid-1);
        quick(array,mid+1,high);
    }
    return array;
 }

对上面的代码进行整合:

void quick_sort(int s[], int l, int r)  
{  
    if (l < r)  
    {  
        //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1  
        int i = l, j = r, x = s[l];  
        while (i < j)  
        {  
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数  
                j--;    
            if(i < j)   
                s[i++] = s[j];  

            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数  
                i++;    
            if(i < j)   
                s[j--] = s[i];  
        }  
        s[i] = x;  
        quick_sort(s, l, i - 1); // 递归调用   
        quick_sort(s, i + 1, r);  
    }  
}  

归并排序

基本思想:将两个或两个以上的有序数据序列合并成一个新的有序数据序列。假设数组A有N个元素,可以将数组A看成是由N个有序的子序列组成,每个子序列的长度为1,然后再两两合并,得到了一个 N/2 个长度为2或1的有序子序列,再两两合并,如此重复,直到得到一个长度为N的有序数据序列为止。

例如无序数组[6 2 4 1 5 9]

第一步 [6 2 4 1 5 9]原始状态
第二步 [2 6] [1 4] [5 9]两两合并排序
第三步 [1 2 4 6] [5 9]继续两组两组合并
第四步 [1 2 4 5 6 9]合并完毕,排序完毕
输出结果[1 2 4 5 6 9]

合并细节
第二步:[2 6] [1 4] [5 9],两两合并,其实仅合并[2 6] [1 4],所以[5 9]不管它

原始状态
第一个数组[2 6]
第二个数组[1 4]
第三个数组[…]

第1步,顺序从第一,第二个数组里取出一个数字:2和1 比较大小后将小的放入第三个数组
第一个数组[2 6]
第二个数组[4]
第三个数组[1]

第2步,继续刚才的步骤,顺序从第一,第二个数组里取数据,2和4,同样的比较大小后将小的放入第三个数组
第一个数组[6]
第二个数组[4]
第三个数组[1 2]

第3步,再重复前边的步骤变成,将较小的4放入第三个数组后变成如下状态

第一个数组[6]
第二个数组[…]
第三个数组[1 2 4]

第4步,最后将6放入,排序完毕
第一个数组[…]
第二个数组[…]
第三个数组[1 2 4 6]

具体算法描述如下:

  1. 将一个列表分割成2个子列表
  2. 第1个列表调用索引[low,mid]来定义, 第2个列表调用索引[mid+1,high]来定义
  3. 将两个有序的子文件R[low..mid)和R[mid+1..high]归并成一个有序的子文件R[low..high]
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定

//将有二个有序数列a[first...mid]和a[mid...last]合并。  
void mergearray(int a[], int first, int mid, int last, int temp[])  
{  
    int i = first, j = mid + 1;  
    int m = mid,   n = last;  
    int k = 0;  

    while (i <= m && j <= n)  
    {  
        if (a[i] <= a[j])  
            temp[k++] = a[i++];  
        else  
            temp[k++] = a[j++];  
    }  

    while (i <= m)  
        temp[k++] = a[i++];  

    while (j <= n)  
        temp[k++] = a[j++];  

    for (i = 0; i < k; i++)  
        a[first + i] = temp[i];  
}  
void mergesort(int a[], int first, int last, int temp[])  
{  
    if (first < last)  
    {  
        int mid = (first + last) / 2;  
        mergesort(a, first, mid, temp);    //左边有序  
        mergesort(a, mid + 1, last, temp); //右边有序  
        mergearray(a, first, mid, last, temp); //再将二个有序数列合并  
    }  
}  

bool MergeSort(int a[], int n)  
{  
    int *p = new int[n];  
    if (p == NULL)  
        return false;  
    mergesort(a, 0, n - 1, p);  
    delete[] p;  
    return true;  
}

堆排序

二叉堆的定义:二叉堆是完全二叉树或者是近似完全二叉树。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆
这里写图片描述

堆排序的过程:
1. 创建一个堆
2. 把堆顶元素(最大值)和堆尾元素互换
3. 把堆的尺寸缩小1,并调用从新的堆顶元素开始进行堆调整
4. 重复步骤2,直到堆的尺寸为1

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。

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

int heapsize;

// 交换A[i]和A[j]
void exchange(vector<int> &A, int i, int j)
{
    int temp = A[i];
    A[i] = A[j];
    A[j] = temp;
}

// 堆调整函数(这里使用的是最大堆),调整第i个结点
void heapify(vector<int> &A, int i)
{
    int leftchild = 2 * i + 1;          // 左孩子索引
    int rightchild = 2 * i + 2;         // 右孩子索引
    int largest;                        // 选出当前结点与左右孩子之中的最大值
    if (leftchild < heapsize && A[leftchild] > A[i])
        largest = leftchild;
    else
        largest = i;
    if (rightchild < heapsize && A[rightchild] > A[largest])
        largest = rightchild;
    if (largest != i)   //最大值结点不是当前结点i
    {
        exchange(A, i, largest);        // 把当前结点和它的最大(直接)子节点进行交换
        heapify(A, largest);            // 递归调用,继续从当前结点向下进行堆调整
    }
}

void buildMaxHeap(vector<int> &num, int n){
    heapsize = n;   //堆大小
    for(int i = heapsize/2-1; i >= 0; i--) // 对每一个非叶结点
        heapify(num, i);                  // 不断的堆调整
}

//堆排序
void heapsort(vector<int> &A, int n)
{
    buildMaxHeap(A, n);
    for(int i=n-1; i >= 1; i--)
    {
        exchange(A, 0, i); // 将堆顶元素(当前最大值)与堆的最后一个元素互换(该操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法)
        heapsize--;        // 从堆中去掉最后一个元素
        heapify(A, 0);     // 从新的堆顶元素开始进行堆调整
    }
}

int main(){
    int a[] = {9,12,17,30,50,20,60,65,4,49};
    vector<int> num (a,a+10);
    int len = num.size();
    heapsort(num, len);
    for(int i=0; i<len; i++){
        cout<<num[i]<<'\t';
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值