目录
排序的“稳定性”概念:
假设a[i]=a[j],且i<j(即序列在排序前元素i在元素j的前面)。如果排序后元素i任在元素j前面,则称排序方法是稳定的,反之,则成该排序算法不稳定。
简单排序算法:
1.冒泡排序:
依次比较相邻两个元素,优先级高(或大或小)的元素向后移动,直至到达序列末尾,无序区间就会相应地缩小。下一次再从无序区间进行冒泡操作,依此循环直至无序区间为1,排序结束。
void bubbleSort(int src[], int n){ //冒泡排序
for (int i = 0; i < n; i++){
for (int j = n - 1; j > i; j--){ //依次从最后1位到第i位,两两对比。
if (src[j] < src[j - 1]){
int temp = src[j]; //将小的往前移
src[j] = src[j - 1];
src[j - 1] = temp;
}
}
}
}
分析总结:冒泡排序的时间复杂度也比较高,为O(n^2),每次遍历无序区间都将优先级高的元素移动到无序区间的末尾。冒泡排序是一种稳定的排序方式。
1.2、冒泡排序优化版:(添加标记,进行判断,当序列的顺序已经全部排序好时,提前终止循环)
void bubbleSort_ad(int src[], int n){ //冒泡排序优化版
/*增加判断,当序列已经有序时,则结束后面的循环*/
bool b = true; //bool作为标记
for (int i = 0; i < n && b; i++){
b = false;
for (int j = n - 1; j > i; j--){
if (src[j] < src[j - 1]){
int temp = src[j];
src[j] = src[j - 1];
src[j - 1] = temp;
b = true; //当此循环中,有数据交换时b的状态为true,否则为false,代表序列已经排序完毕,将终止后面的循环
}
}
}
}
2.简单选择排序:
进行循环,每次循环挑选出无序序列中最小的元素,并把放在无序序列的最前面。
void Sort::selectSort(int src[], int n){ //简单选择排序算法
/*
①每次从i开始往后循环,选择出循环中最小的元素与i互换位置。
②时间复杂度与冒泡排序相同为o(n^2),但性能略优于冒泡排序
*/
for (int i = 0; i < n; i++){
int min = i;
for (int j = i + 1; j < n; j++){
if (src[j] < src[min]){ //挑选出序列中最小的元素
min = j;
}
}
if (min != i){ //序列第i个元素不是最小值,则将最小值与元素i互换位置
int temp = src[i];
src[i] = src[min];
src[min] = temp;
}
}
}
分析结论:选择排序交换移动数据的次数较少,能够节约相应时间,但无论情况好坏它的比较次数都是一样的(每次循环都需从前往后进行比较),时间复杂度也为o(n^2).
3.插入排序
void Sort::insertSort(int src[], int n){ //直接插入排序算法
/*
直接插入排序算法:
①依次将一个元素插入到已经排序好的序列中,从而得到一个新的元素。
②也就是将第i个元素,依次以倒序的方式与前i-1个元素比较,插入到对应位置。(前i-1个元素已经排序好了)
③时间复杂度为o(n^2),性能比冒泡和简单选择排序要好一些。
*/
int j;
int t;
for (int i = 1; i < n; i++){
if (src[i] < src[i - 1]){
t = src[i]; //设置哨兵,储存当前的第i个元素
for (j = i - 1; j >= 0 && src[j] > t; j--){
src[j + 1] = src[j];
}
src[j + 1] = t;
}
}
}
改进算法:
4.希尔排序
void Sort::shellSort(int src[], int n){
/*
希尔排序思想:首先序列分成若干个大小为gap的组,然后从对每个组进行插入排序。
算法复杂度:时间复杂度为o(n^(3/2)),为不稳定排序。
算法实现步骤:
①算法在进行插入排序时,对单个元素的插入不是按照传统的方式一个组一个组的进行插入排序,
②而对每个组第1个元素进行插入,然后对每个组第2个元素进行插入。这样依次进行。
③每组第0个元素时默认排序好的。
④分组方式:以gap=4为例,则分组为{0、4、8},{1、5、9},{2、6、10},{3、7、11},切记勿把
{0、1、2、3}当成一组。
*/
int temp; //定义中间变量,做为临时交换变量
//遍历数组(进行排序)
int gap = n;
do{
//初始增量变化规律
gap = gap / 3 + 1;
for (int i = gap; i < n; i++) //此for循环,先对第1组的第1个元素插入排序,再对第2组的第一个元素插入排序。
{
if (src[i] < src[i - gap]){
temp = src[i];
int j;
for (j = i; j >= gap && temp < src[j - gap]; j -= gap){ //j>=gap防止for循环导致数组越界
src[j] = src[j - gap]; //将比temp大的元素往后移
}
//此for循环等同于下面这个
//for (j = i; j >= gap; j -= gap){ //j>=gap防止for循环导致数组越界
// if (temp < src[j - gap]){
// src[j] = src[j - gap]; //将比temp大的元素往后移
// }
// else{
// break;
// }
//}
src[j] = temp; //把temp插入到正确的位置
}
}
} while (gap > 1); //再进行第一次循环之后,序列基本有序,但是第i*(0~gap)元素之间无序,这时我们缩小gap的值,再次分组进行插入。一直到gap=1时结束。这时排序才最终完成
}
5.堆排序
时间复杂度:堆排序的时间复杂度为 O(nlogn) 。
void Sort::HeapAjust(int src[], int root, int n){
int child = 0;
while (2 * root + 1 < n){ //判断当前root是否有孩子
child = 2 * root + 1;
if (child + 1 < n){ //判断是否存在右孩子
if (src[child] < src[child + 1]) //如果左孩子小于右孩子,则将索引移到右孩子身上
child++;
}
if (src[root] < src[child]){ //如果根小于孩子,则互换其元素
Swap(src[root], src[child]);
root = child; //然后以当前孩子为根继续往下构建
}
else
break; //当根的值比孩子大时结束while语句
}
}
void Sort::HeapSort(int src[], int n){//堆排序
/*
堆排序:时间复杂度为O(nlogn)
算法步骤:首先对整个序列构建一个大顶堆,然后将根节点移走放在序列末尾(也就是序列最大的元素),
再对剩下的n-1个元素继续构建大顶堆,然后再把当前大顶堆的根节点移走。依次循环往复。
注意:首次构建大顶堆时,从树的最后一个父节点开始(i=n/2-1),依次往树的顶部循环(i--)。
*/
int i;
//初始化堆,从最后一个父节点开始
for (i = n / 2 - 1; i >= 0; i--){
HeapAjust(src, i, n);
}
//从堆中的取出最大的元素再调整堆
int j;
for (j = n - 1; j > 0; j--){
Swap(src[j], src[0]); //将根元素与树的最后一个元素互换
HeapAjust(src, 0, j); //然后对剩下的
}
}
分析结论:由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为 o(nlogn)。这在性能
上显然要远远好过于冒泡、简单选择、 直接插入的 O(n2)的时间复杂度了。
6.归并排序
时间复杂度:归并排序的时间复杂度为 O(nlogn) 。
空间复杂度:O(n);需要开辟一个n个元素的辅助数组,以空间换取时间。
归并排序的思想:
step1.先通过二分法递归将数组拆分为单个的元素,进行log2(N)次递归即将数组拆分为单个元素。递归二分法步骤时间消耗logn
step2.然后从每一层使用O(n)级别的算法进行排序。每层时间消耗n;
1)step1示意图:经log2(8)=4次递归,将数组拆分为单个元素。
2)step2示意图:将每层的元素归并到数组中。
//归并函数
void Merge(int a[], int L, int M, int R){
//--确定left和right数组的大小
int n1 = M - L + 1;
int n2 = R - M;
int* Left = new int[n1]; //动态分配n个整型存储单元,这些单元的首地址赋给定义的整型指针Left
int* Right = new int[n2];
//--将值赋给left和right数组
for (int i = 0; i<n1; i++){
Left[i] = a[i + L];
}
for (int i = 0; i<n2; i++){
Right[i] = a[i + M + 1];
}
int i, j, k; //双指针移动
//--进行循环依次将lift和right中的值有序的移动到a数组中
for (i = 0, j = 0, k = L; i < n1 && j < n2; k++){ //当n1和n2其中任意一个数组中的元素被选用完时,结束循环
if (Left[i] < Right[j]){
a[k] = Left[i];
i++;
}
else{
a[k] = Right[j];
j++;
}
}
//--归并上一步循环剩下的数组数据
if (i<n1){ //此时如果n1中还有元素,则将n1的剩余元素值依次赋给a
for (int m = i; m < n1; m++, k++)
a[k] = Left[m];
}
if (j<n2){ //此时如果n2中还有元素,则将n1的剩余元素值依次赋给a
for (int m = j; m < n2; m++, k++)
a[k] = Right[m];
}
}
//归并排序
void MergeSort(int a[], int L, int R) {
if (L < R) {
int Mid = (L + R) / 2;
MergeSort(a, L, Mid); //二分法递归拆分为两个数组
MergeSort(a, Mid + 1, R);
if (a[Mid] > a[Mid + 1]) ///优化1.当左右两个数组已经有序时,则不需要归并操作
Merge(a, L, Mid, R); //然后将上面拆开的两个数组合并为一个有序的数组
}
}
7.快速排序
方法1:
方法2:
void Sort::quickSort(int a[], int left,int right){
/*快速排序:
基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,
则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
*/
if (left > right){
return;
}
//==此处是选取一个数值较中等的基准数。(这样使选取的基准数不会过小或过大,提高后面排序的性能)
int mid = (left + right) / 2;
if (a[left] > a[right])
Swap(a[left], a[right]);
if (a[mid] > a[right])
Swap(a[mid], a[right]);
if (a[mid] > a[left])
Swap(a[left], a[mid]);
//==正式开始排序
int temp = a[left]; //temp为基准数
int i = left;
int j = right;
while (i != j){
//-顺序很重要,先从右往左开始
while (a[j] >= temp && i<j)
j--;
//-再从左往右
while (a[i] <= temp&& i<j)
i++;
//-交换两个数的顺序
if (i != j){ //此时a[left]大于基准数,a[right]小于基准数。所以需要调换顺序
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
//==最终找到当前基准数在数组的位置
a[left] = a[j];
a[j] = temp;
//==继续处理j左边的序列
quickSort(a,left, j - 1);
//==继续处理j右边的序列
quickSort(a, j + 1, right);
}