排序
点击以下图片查看大图:
冒泡排序
1、比较相邻的元素,如果前一个比后一个大或者小,就把它们两个交换位置。
2、对每一对相邻的元素做同样的工作,从开始第一对到最后一对,这步做完之后,最后的元素会是最大或者最小的数。
3、针对所有的元素重复以上的步骤,除了最后一个
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
空间复杂度:O(1)
时间复杂度:O(n^2)
void swap1(int *a,int i,int j)
{
int tmp = a[i];
a[i]=a[j];
a[j]=tmp;
}
void swap2(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
/*
*a=*a^*b;
*b=*a^*b;
*a=*a^*b;
*a=*a+*b; //*a=*a**b;
*b=*a-*b; //*b=*a/*b;
*a=*a-*b; //*a=*a/*b;
*/
}
void BubbleSort(int *a,int len)
{
int i,j;
int flag;
for(i=0;i<len-1;i++)
{
flag=1;
for(j=0;j<len-1-i;j++)
{
if(a[j] > a[j+1])
{
// swap1(a,j,j+1);
swap2(&a[j],&a[j+1]);
flag=0;
}
}
if(flag) //一趟下来没有交换,说明已经有序
return;
}
}
鸡尾酒排序
冒泡排序的改进:鸡尾酒排序
鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进,此算法与冒泡排序的不同处在与从低到高然后从高到低,而冒泡排序仅从低到高去比较序列里的每个元素。
鸡尾酒排序的优点是能够在特定条件下(如集合中大部分元素有序),减少排序的回合数。
空间复杂度:O(1)
时间复杂度:O(n^2)
void CocktailSort(int *a,int len)
{
int l=0; //边界
int r=len-1;
int i;
while(l<r) //(n-1)/2
{
//前半轮 将最小的元素放到后面
for(i=l;i<r;i++)
{
if(a[i] < a[i+1])
swap2(&a[i],&a[i+1]);
}
r--;
//后半轮 将最大的元素方在前面
for(i=r;i>l;i--)
{
if(a[i] > a[i-1])
swap2(&a[i],&a[i-1]);
}
l++;
}
}
选择排序:
一种简单直观的排序算法,初始时在序列中找到最小(大)元素,放在序列起始位置作为已排序序列,然后,在从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。
空间复杂度:O(1)
时间复杂度:O(n^2)
void SelectSort(int *a,int len)
{
int i,j;
for(i=0;i<len-1;i++)
{
int max = i;
for(j=i+1;j<len;j++)
{
if(a[j]>a[max])//获取最大元素下标
max=j;
}
if(max != i)
swap2(&a[max],&a[i]);
}
}
简单插入排序:
插入排序是一种简单直观的排序算法,它的工作原理非常类似我们抓扑克牌
对于未排序的数据(右手拍牌),对于已排序的序列(左手拿的排行顺序的牌),在拿数据的时候,直接根据左手排序的数从右往左扫描,找到对应位置插入
1、第一个元素开始,该元素可以认为已经被排序
2、取下一个元素,在已经排序的元素序列中从右往左扫描
3、如果取出来的元素,比已排序元素要小,将该元素往左移到它指定的位置
4、重复3步骤,直到找到已排序的元素小于或等于新元素的位置
5、将新元素插入到该位置
6、重复执行2~5步骤
空间复杂度:O(1)
时间复杂度:O(n^2)
void InsertSort(int *a,int len)
{
int i,j,g;
for(i = 1;i < len;i++)
{
g = a[i]; //获取元素
j = i-1;
while(j >= 0 && a[j] < g) //判断插入位置
{
a[j+1] = a[j]; //元素后移
j--;
}
a[j+1] = g; //插入元素
}
}
二分插入排序
适用于数据量比较大的情况
//分成两部分前半部分有序通过二分查找,后半部分无序
//开始排序时前半部分仅有一个元素
//1.先保存需要插入元素
//2.搜索前半部分需要插入的位置
//3.在插入位置后面的元素整体后移一位
//4.在指定位置插入元素
void BinarySearchInsertSort(int *a,int len)
{
int i,j,g,left,right,mid;
for(i=1;i<len;i++)
{
g=a[i];
left=0;
right=i-1;
while(left <= right)
{
mid=left + (right -left)/2;
if(a[mid] < g)
left = mid+1;
else
right = mid-1;
}
for(j=i-1;j>=left;j--)
a[j+1]=a[j];
a[left]=g;
}
}
快速排序:
快速排序是一种“分治法”。它将原本的问题分成两个子问题(比基准值小的数和比基准值大的数),然后再分别解决这两个问题。子问题,也就是子序列完成排序后,再像一开始说明的那样,把他们合并成一个序列,那么对原始序列的排序也就完成了。 不过,解决子问题的时候会再次使用快速排序,甚至在这个快速排序里仍然要使用 快速排序。只有在子问题里只剩一个数字的时候,排序才算完成
分割子序列时需要选择基准值,如果每次选择的基准值都能使得两个子序列的长度为原本的一半,那么快速排序的运行时间和归并排序的一样,都为 O(nlogn)。和归并排序类似,将序列对半分割 log2n 次之后,子序列里便只剩下一个数据,这时子序列的排序也就完成了。因此,如果像下图这样一行行地展现根据基准值分割序列的过程,那么总共会有 log2n 行。
每行中每个数字都需要和基准值比较大小,因此每行所需的运行时间为 O(n)。由此可知,整体的时间复杂度为 O(nlogn)。如果运气不好,每次都选择最小值作为基准值,那么每次都需要把其他数据移到基
准值的右边,递归执行 n 行,运行时间也就成了 O(n^2)。这就相当于每次都选出最小值并把它移到了最左边,这个操作也就和选择排序一样了。此外,如果数据中的每个数字被选为基准值的概率都相等,那么需要的平均运行时间为 O(nlogn)。
实现思路:
1、从序列中挑出一个元素,作为“基准值”
2、把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在后面,相同放到任一边,就好比分区操作
3、对1~2步骤进行递归
注意:实现快速排序的关键在于先在数组中选择一个数字,然后把数组中的数字分为两部分,比选择的数字小的移到数组的左边,比选择的数字大的一道数组的右边
void swap(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
void quick_sort(int *a,int low,int high)
{
if(low>=high)
return;
/*
* 为什么要定义first和last来代替low和high呢,
* 因为后续左右区间元素还需要用到low和high两个变量
* 至于key值得引入,其实用a[low]代替完全没有影响
*/
int first=low;
int last=high;
int key=a[low];//基准值
while(first<last)
{
while(a[last] >= key && first < last)
last--;
while(a[first] <= key && first < last)
first++;
if(first!=last)
swap(&a[first],&a[last]);
else
swap(&a[low],&a[last]); //基准值交换
}
quick_sort(a,low,first);
quick_sort(a,first+1,high);
}
希尔排序:
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
希尔排序的代码如下:
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
// 最优时间复杂度 ---- O(n)
// 平均时间复杂度 ---- 根据步长序列的不同而不同。
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void ShellSort1(int *a,int len)
{
int gap,i,j,target;
for(gap=len/2;gap>0;gap/=2)
{
for(i=gap;i<len;i++)
{
target=a[i];
j=i-gap;
while(j>-1 && a[j]>target)
{
a[j+gap]=a[j];
j-=gap;
}
a[j+gap]=target;
}
}
}
void ShellSort(int A[], int n)
{
int h = 0;
while (h <= n) // 生成初始增量
{
h = 3 * h + 1;
}
while (h >= 1)
{
for (int i = h; i < n; i++)
{
int j = i - h;
int get = A[i];
while (j >= 0 && A[j] > get)
{
A[j + h] = A[j];
j = j - h;
}
A[j + h] = get;
}
h = (h - 1) / 3; // 递减增量
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大希尔排序
int n = sizeof(A) / sizeof(int);
ShellSort(A, n);
printf("希尔排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
归并排序:
归并排序是创建在归并操作上的一种有效的排序算法,效率为O(nlogn),1945年由冯·诺伊曼首次提出。
归并排序的实现分为递归实现与非递归(迭代)实现。递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
归并排序的代码如下:
#include <stdio.h>
#include <limits.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定
void Merge(int A[], int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
{
int len = right - left + 1;
int *temp = new int[len]; // 辅助空间O(n)
int index = 0;
int i = left; // 前一数组的起始元素
int j = mid + 1; // 后一数组的起始元素
while (i <= mid && j <= right)
{
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid)
{
temp[index++] = A[i++];
}
while (j <= right)
{
temp[index++] = A[j++];
}
for (int k = 0; k < len; k++)
{
A[left++] = temp[k];
}
}
void MergeSortRecursion(int A[], int left, int right) // 递归实现的归并排序(自顶向下)
{
if (left == right) // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
return;
int mid = (left + right) / 2;
MergeSortRecursion(A, left, mid);
MergeSortRecursion(A, mid + 1, right);
Merge(A, left, mid, right);
}
void MergeSortIteration(int A[], int len) // 非递归(迭代)实现的归并排序(自底向上)
{
int left, mid, right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
for (int i = 1; i < len; i *= 2) // 子数组的大小i初始为1,每轮翻倍
{
left = 0;
while (left + i < len) // 后一个子数组存在(需要归并)
{
mid = left + i - 1;
right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够
Merge(A, left, mid, right);
left = right + 1; // 前一个子数组索引向后移动
}
}
}
int main()
{
int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大归并排序
int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n1 = sizeof(A1) / sizeof(int);
int n2 = sizeof(A2) / sizeof(int);
MergeSortRecursion(A1, 0, n1 - 1); // 递归实现
MergeSortIteration(A2, n2); // 非递归实现
printf("递归实现的归并排序结果:");
for (int i = 0; i < n1; i++)
{
printf("%d ", A1[i]);
}
printf("\n");
printf("非递归实现的归并排序结果:");
for (int i = 0; i < n2; i++)
{
printf("%d ", A2[i]);
}
printf("\n");
return 0;
}
堆排序:
堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。
我们可以很容易的定义堆排序的过程:
由输入的无序数组构造一个最大堆,作为初始的无序区
把堆顶元素(最大值)和堆尾元素互换
把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
重复步骤2,直到堆的尺寸为1
堆排序的代码如下:
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void Heapify(int A[], int i, int size) // 从A[i]向下进行堆调整
{
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 选出当前结点与其左右孩子三者之中的最大值
if (left_child < size && A[left_child] > A[max])
max = left_child;
if (right_child < size && A[right_child] > A[max])
max = right_child;
if (max != i)
{
Swap(A, i, max); // 把当前结点和它的最大(直接)子节点进行交换
Heapify(A, max, size); // 递归调用,继续从当前结点向下进行堆调整
}
}
int BuildHeap(int A[], int n) // 建堆,时间复杂度O(n)
{
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
Heapify(A, i, heap_size);
return heap_size;
}
void HeapSort(int A[], int n)
{
int heap_size = BuildHeap(A, n); // 建立一个最大堆
while (heap_size > 1) // 堆(无序区)元素个数大于1,未完成排序
{
// 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
// 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
Swap(A, 0, --heap_size);
Heapify(A, 0, heap_size); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大堆排序
int n = sizeof(A) / sizeof(int);
HeapSort(A, n);
printf("堆排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
参考:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html