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

来源:互联网 发布:淘宝客退款佣金怎么算 编辑:程序博客网 时间:2024/06/04 17:53

  • 简述
    • 插入排序
    • 冒泡排序
    • 选择排序
    • 希尔排序
    • 快速排序
    • 归并排序
    • 堆排序


简述

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

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

这里写图片描述


插入排序

  • 直接插入
  • 二分插入

(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 0
原创粉丝点击