一. 冒泡排序
- 它重复地走访过要排序的元素列,一次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。
- 依次比较相邻的数据,将小数据放在前,大数据放在后;即第一趟先比较第1个和第2个数,大数在后,小数在前,再比较第2个数与第3个数,大数在后,小数在前,以此类推则将最大的数"滚动"到最后一个位置;第二趟则将次大的数滚动到倒数第二个位置......第n-1(n为无序数据的个数)趟即能完成排序。
- 平均时间复杂度:O(n^2)
空间复杂度:O(1) (用于交换)
稳定性:稳定
是否为原地排序:是
#include <iostream>
using namespace std;
int bubble_sort(int *arr,int num) {
int temp,flag;
for (int i = 0; i < num-1; i++) {
flag = 0;
for (int j = 0; j < num-i-1; j++) {
//从小到大的排序
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 1;
}
}
//优化,如果发现这一趟没有进行过任何交换,说明已经完成排序,可以退出
if (flag == 0)
break;
}
return 1;
}
int main() {
int arr[11] = { 1,5,56,89,88,21,99,152,64,-12,45 };
bubble_sort(arr, 11);
for (int i = 0; i < 11; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
二. 选择排序
- 选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
- 选择排序:比如在一个长度为N的无序数组中,在第一趟遍历N个数据,找出其中最小的数值与第一个元素交换,第二趟遍历剩下的N-1个数据,找出其中最小的数值与第二个元素交换......第N-1趟遍历剩下的2个数据,找出其中最小的数值与第N-1个元素交换,至此选择排序完成。
- 平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
是否为原地排序:是
#include <iostream>
using namespace std;
int select_sort(int *arr,int num) {
int loc,temp;
for (int i = 0; i < num-1; i++) {
loc = i;
//查找这一趟最小的元素
for (int j = i+1; j < num; j++) {
if (arr[loc] > arr[j])
loc = j;
}
//如果有比目前位置更小的,交换
if (loc != i) {
temp = arr[i];
arr[i] = arr[loc];
arr[loc] = temp;
}
}
return 1;
}
int main() {
int arr[11] = { 1,5,56,89,88,21,99,152,64,-12,45 };
select_sort(arr, 11);
for (int i = 0; i < 11; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
三. 插入排序
- 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序。从第二个元素开始,逐个插入到左边的有序数据中。
- 与选择排序一样,当前索引左边的所有元素都是有序的,但它们的最终位置还没确定。插入排序的时间是不确定的,如果对一个很大,并且其中已经有序或接近有序的元素,排序会比随机顺序或逆序数组快得多。
- 平均时间复杂度:介于N和N^2之间
空间复杂度:O(1)
稳定性:稳定
是否为原地排序:是
#include <iostream>
using namespace std;
int insert_sort(int *arr,int num) {
int temp,i,j;
for (i = 1; i < num; i++) {
temp = arr[i];
//发现前面的数大于当前要插入的数,则把数往后移一位
for (j = i; j > 0 && arr[j-1]>temp; j--) {
arr[j] = arr[j-1];
}
arr[j] = temp;
}
return 1;
}
int main() {
int arr[11] = { 1,5,56,89,88,21,99,152,64,-12,45 };
insert_sort(arr, 11);
for (int i = 0; i < 11; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
四. 希尔排序
- 希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
- 就是说,每次排序都有一个增量,按照增量,分成几个组来排序。等到增量为1的排序开始时,整个数组已经变得比较有序了。
当增量为5的时候,有5个分组。如上图所示,同序号的为同一分组,分别对每个分组进行插入排序。然后减少增量重复上述过程,直到增量为1结束。 - 平均时间复杂度:未知(学界尚在研究,性能暂时无法准确描述)
空间复杂度:O(1)
稳定性:不稳定
是否为原地排序:是
#include <iostream>
using namespace std;
int shell_sort(int *arr,int num) {
int gap,i,j,k,temp;
//每次增量为1/2,直到增量为1
for (gap = num / 2; gap >= 1; gap /= 2) {
//每一次有和增量同数目的分组要进行排序
for (i = 0; i < gap; i++) {
//开始对该分组进行插入排序
for(j=i+gap;j<num;j+=gap){
temp = arr[j];
for (k = j; k > i&&arr[k - gap] > temp; k -= gap) {
arr[k] = arr[k - gap];
}
arr[k] = temp;
}
}
}
return 1;
}
int main() {
int arr[15] = { 1,5,56,89,88,21,99,152,64,-12,45,68,451,456,78 };
shell_sort(arr, 15);
for (int i = 0; i < 15; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
五. 归并排序
- 归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 二路归并原理:
①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
②设定两个指针,最初位置分别为两个已经排序序列的起始位置
③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④重复步骤3直到某一指针超出序列尾
⑤将另一序列剩下的所有元素直接复制到合并序列尾 - 排序原理:
其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。如何让这二组组内数据有序?
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。 - 时间复杂度:N*logN
空间复杂度:O(N)
稳定性:稳定
是否为原地排序:否
#include <iostream>
using namespace std;
//归并两个有序数组
int merge(int *in_arr, int start, int mid, int end, int *sort_arr) {
int i = start;
int start_u = start; int mid_u = mid+1;
//如果左右两部分均未到达边界,比较大小,取小的放入排序好的函数
while (start_u!=mid+1 && mid_u!=end+1) {
if (in_arr[start_u] > in_arr[mid_u])
sort_arr[i++] = in_arr[mid_u++];
else
sort_arr[i++] = in_arr[start_u++];
}
//如果有一个到达边界,判断是哪一个,并把另外一个没到边界的剩余数字放入目标中
while (start_u != mid + 1)
sort_arr[i++] = in_arr[start_u++];
while (mid_u != end + 1)
sort_arr[i++] = in_arr[mid_u++];
//排序好的数组放入目的数组中
for (int j = start; j <= end; j++) {
in_arr[j] = sort_arr[j];
}
return 1;
}
//依次递归,直到剩下一个值为止
void mergesort(int *in_arr, int start, int end, int *sort_arr) {
if (start < end) {
int mid = (end + start) / 2;
//cout << "Start:" << start << " End:" << end << endl;
mergesort(in_arr, start, mid, sort_arr); // 左边排序
mergesort(in_arr, mid + 1, end, sort_arr); //右边排序
merge(in_arr, start, mid, end, sort_arr); //合并
}
}
//分配空间,并调用递归的归并排序函数
int merge_sort(int *arr, int num) {
int *sort_arr = new int[num];
if (sort_arr == NULL)
return -1;
mergesort(arr, 0, num - 1, sort_arr);
delete[] sort_arr;
return 1;
}
//主函数
int main() {
int arr[15] = { 1,5,56,89,88,21,99,152,64,-12,45,68,451,456,78 };
merge_sort(arr, 15);
for (int i = 0; i < 15; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
六. 快速排序
- 快速排序由C. A. R. Hoare在1962年提出。它采用了分治法,基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 该方法的基本思想是:
①先从数列中取出一个数作为基准数。
②分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
③再对左右区间重复第二步,直到各区间只有一个数。 - 假设用户输入了如下数组:
①创建变量i=0(指向第一个数据), j=5(指向最后一个数据), k=6(赋值为第一个数据的值)。0 1 2 3 4 5 6 2 7 3 8 9
②以第一个6为基数,从j开始,从右往左找,不断递减变量j的值,我们找到第一个下标3的数据比6小,于是把数据3移到下标0的位置,把下标0的数据6移到下标3,完成第一次比较(此时i=0 j=3 k=6):
③接着,开始第二次比较,这次要变成找比k大的了,而且要从前往后找了。递加变量i,发现下标2的数据是第一个比k大的,于是用下标2的数据7和j指向的下标3的数据的6做交换,数据状态变成下表(i=2 j=3 k=6):0 1 2 3 4 5 3 2 7 6 8 9
④上面从又到左、再从左到右的比较为一个循环。不断重复这种循环,直到i==j。0 1 2 3 4 5 3 2 6 7 8 9
⑤然后对左右两部分分别再进行快排,直到左右两部分只剩下1个或0个数为止。
- 时间复杂度:N*logN
空间复杂度:lgN
稳定性:不稳定
是否为原地排序:是
#include <iostream>
using namespace std;
int quicksort(int *arr, int start, int end) {
int start_u=start, end_u=end;
int standard = arr[start], loc = start;
while (start_u != end_u) {
//从右到左找比temp小的
while (arr[end_u] > standard) {
end_u--;
if (end_u==start_u)
goto exit;
}
arr[loc] = arr[end_u];
arr[end_u] = standard;
loc = end_u;
//从左到右找比temp大的
while (arr[start_u] < standard) {
start_u++;
if (end_u == start_u)
goto exit;
}
arr[loc] = arr[start_u];
arr[start_u] = standard;
loc = start_u;
}
exit:
//除非已经不可再排了,否则对左右两部分再进行快排
if (loc != start && loc-1 != start)
quicksort(arr, start, loc - 1);
if (loc != end && loc + 1 != end)
quicksort(arr, loc + 1, end);
return 1;
}
int quick_sort(int *arr, int num) {
quicksort(arr,0,num-1);
return 1;
}
int main() {
int arr[15] = { 1,5,56,89,88,21,99,152,64,-12,45,68,451,456,78 };
quick_sort(arr, 15);
for (int i = 0; i < 15; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
七. 堆排序
- 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
- 二叉堆:
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足二个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
由于其它几种堆(二项式堆,斐波纳契堆等)用的较少,一般将二叉堆就简称为堆。 - 关于堆的构建、插入、删除操作,可以看这篇文章:数据结构—堆
- 堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
- 时间复杂度:N*logN
空间复杂度:1
稳定性:不稳定
是否为原地排序:是
(下列程序从百度百科摘抄来的,整篇里面唯一不是自己写的,因为我自己还不太完全理解堆排序)
#include <iostream>
using namespace std;
#include <stdio.h>
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
};
void adjustHeap(int param1, int j, int inNums[]);
void heap_sort(int inNums[], int nums);
//大根堆进行调整
void adjustHeap(int param1, int j, int inNums[])
{
int temp = inNums[param1];
for (int k = param1 * 2 + 1; k<j; k = k * 2 + 1)
{
//如果右边值大于左边值,指向右边
if (k + 1<j && inNums[k]< inNums[k + 1])
{
k++;
}
//如果子节点大于父节点,将子节点值赋给父节点,并以新的子节点作为父节点(不用进行交换)
if (inNums[k]>temp)
{
inNums[param1] = inNums[k];
param1 = k;
}
else
break;
}
//put the value in the final position
inNums[param1] = temp;
}
//堆排序主要算法
void heap_sort(int inNums[],int nums)
{
//1.构建大顶堆
for (int i = nums / 2 - 1; i >= 0; i--)
{
//put the value in the final position
adjustHeap(i, nums, inNums);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for (int j = nums - 1; j>0; j--)
{
//堆顶元素和末尾元素进行交换
int temp = inNums[0];
inNums[0] = inNums[j];
inNums[j] = temp;
adjustHeap(0, j, inNums);//重新对堆进行调整
}
}
int main() {
int arr[15] = { 1,5,56,89,88,21,99,152,64,-12,45,68,451,456,78 };
heap_sort(arr, 15);
for (int i = 0; i < 15; i++) {
cout << arr[i] << " ";
}
cout << endl;
getchar();
return 0;
}
八. 基数排序
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
- 时间复杂度:O(d(n+radix)) 【n是待排序的n个记录,d表示d个关键码(下面例子为3),radix是桶数桶数】
空间复杂度:O(radix*n)
稳定性:稳定
是否为原地排序:否
- 举个栗子:对64、8、216、512、27、729、0、1、343、125
如果我们取基数为10,则需要建立10个桶。然后用次位优先法(这里次位只最低位,主位只最高位,次位优先就是从最低位开始),从低位到高位每次按顺序扫描排序。过程如下:
桶 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
第1趟桶内存放 | 0 | 1 | 512 | 343 | 64 | 125 | 216 | 27 | 8 | 729 |
第2趟桶内存放 | 0、1、8 | 512、216 | 125、27、729 | 343 | 64 | |||||
第3趟桶内存放 | 0、1、8、27、64 | 125 | 216 | 343 | 512 | 729 |
从低位开始,扫到高位结束后,直接按顺序把每个桶里数据输出,得到排序完毕的序列:0、1、8、27、64、125、216、343、512、729。
九. 各种排序方法对比
算法 | 是否稳定 | 是否原地排序 | 时间复杂度 | 空间复杂度 | 备注 |
---|---|---|---|---|---|
冒泡排序 | 是 | 是 | N^2 | 1 | |
选择排序 | 否 | 是 | N^2 | 1 | |
插入排序 | 是 | 是 | N~N^2 | 1 | |
希尔排序 | 否 | 是 | 未知 | 1 | |
归并排序 | 是 | 否 | N*logN | lgN | |
快速排序 | 否 | 是 | N*logN | lgN | |
堆排序 | 否 | 是 | N*logN | 1 | |
基数排序 | 是 | 否 | O(d(N+radix)) | O(radix*N) |
- 大多数情况下, 都会选择快速排序(尤其是三向快速排序)。如果稳定性很重要,并且空间不是问题,会选择归并排序。
- 稳定性:如果一个排序算法能够保留数组中重复元素的相对位置,则可以被称为是稳定的。例如:在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。它的重要性可能体现在一些具有多种键值的排序中。例如一个数组排序前每一个元素是按照时间顺序排好的,此时再对其地理位置排序,排完之后如果需要相同地理位置时间是按顺序的,就需要选择稳定算法。