排序的重要性不言而喻,让我们看看一些常见的排序,这里我们都以升序为例:
一、直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],.,array[i-1]已经排好序,此时用array[i的排序码与array[i-1]array[i-2].的排序码顺序进行比较,找到插入位置即将arrayU插入,原来位置上的元素顺序后移.
代码及注释如下:
void Insertsort(int* arr, int n)//arr数组,n元素个数
{
for (int i = 0; i < n-1; i++)//循环n次
{
int end = i;//将i的值赋给end,方便将其数值改变而不影响循环
int tmp = arr[end + 1];//由于升序,我们要提前保存要排序的数
//这里默认前i个数是已排好的,即end
while (end >= 0)
{
if (arr[end] > tmp)//arr[end]与arr[end+1]比较
{
arr[end + 1] = arr[end];//满足条件赋值
end--;//继续向前排列
}
else
{
break;//不满足条件退出循环
}
}
arr[end + 1] = tmp;//将保存的数值赋给留出的位置
}
}
可以用以下代码测试:
void Print(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 2,4,6,8,0,1,3,5,7,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
Insertsort(arr, sz);
Print(arr, sz);
}
结果也显而易见:
直接插入排序是通过进行比较来插入的,最坏的情况就是都要比较,所以是O(N^2),最好情况就是本生就是顺序且有序的。
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
二、希尔排序
希尔排序大家现在当下只需知道大概在O(1.3N)左右即可
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定,大家就记为 O(1.3N)
4. 稳定性:不稳定
代码如下:
void Shellsort(int* arr, int n)
{
int gap = n;//将n的值赋值一份给gap,便于后续对gap给值划分
while (gap > 1)
{
//法一:gap/2为单位
gap = gap / 2;//gap的值以二分之一不断划分,最后得到gap=1进行插入排序
//gap/3为单位
//gap = gap / 3 + 1;//gap的值以三分之一不断划分,最后加+1得到gap=1进行插入排序
//gap>1时进行预排序
//gap=1时进行插入排序
for (int i = 0; i < n - gap; i++)//i<n-gap:把间隔为gap的多组数据同时排
{
//下面操作和插入排序大体相同,但注意不再时加减1;而是以gap为单位!!!
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
大家也可以用我插入排序时的代码进行测试,结果也是一样的。
三、选择排序
选择排序是在每一趟的遍历中找到最大的和最小的数,分别与数组末尾和开始的数据进行交换,直到数组有序。
思路比较简单,我们来一起看看代码:
void SelectSort(int* a, int n)
{
int begin = 0;//给定初始位置
int end = n - 1;//给定末位置
while (begin < end)//判断循环结束条件
{
//开始时最大值,最小值都为第一个数据的下标,然后开始比较
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; i++) //从第二个数据开始找
{
if (a[i] < a[mini])
{
mini = i; //小于就交换该位置与mini的下标
}
if (a[i] > a[maxi])
{
maxi = i;//大于就交换该位置与maxi的下标
}
}
Swap(&a[begin], &a[mini]); //进行交换
if (maxi == begin) //防止重复交换导致结果不对
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
四、堆排序
堆排序即利用堆的思想来进行排序,首先要建堆
升序:建大堆
降序:建小堆
代码如下:
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
void AdjustDwon(int* a, int n, int parent)
{
int child = parent * 2 + 1;//得出左孩子下标
while (child < n)
{
//如果右孩子大于左孩子,++得到右孩子
if (child + 1 < n && a[child] < a[child + 1])
{
child++;
}
if (a[parent] < a[child])//比较,孩子大于父亲就交换
{
Swap(&a[parent], &a[child]);
//以孩子的下标为父亲进行新一轮的比较
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//将数组a直接建堆,此时最大的数在堆顶
for (int i = (n - 1 - 1) / 2; i >= 0; i++)
{
AdjustDwon(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]); //交换堆顶和末尾的数,使最大的数到最后
AdjustDwon(a, end, 0); //将剩下的n-1个数继续建堆,不影响已经选出的最大数
end--;
}
}
堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
五、冒泡排序
这是一个可以说是最简单的排序了,实现的关键就是想好两层循环的条件就行了,它只具有教学意义,因为时间复杂度太高了。
代码如下:
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
int flag = 1; //如果发生交换,flag=0,如果这次没发生交换,说明已经有序
for (int i = 0; i < n - 1 - j; i++) //每排好一次,里面循环就少排一次,所以是n-1-j
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
flag = 0;
}
}
if (flag)
break;
}
}
1、时间复杂度:O(N^2)
2、稳定性:稳定
六、快速排序
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
先学hoare版本
注意:hoare的版本是将key取开头的元素
如果我们按照升序排列,我们要先走右边的,这样才能保证相遇的点其值一定比key点的小,原因如下:
相遇分为以下两种情况:
1.左边相遇右边,即右边停止后左边一直走,没有找到比key大的元素直到相遇,而相遇点是右边找小找到的,说明相遇点比key点小(升序)
2.右边相遇左边,即左边停止后右边一直走,没有找到比key小的元素直到相遇,而相遇点是左边找大找到的,说明相遇点比key点大。(降序)
代码如下:
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
//三数取中,得到中间大的数
int GedMidi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[right] < a[left])
{
return left;
}
else
return right;
}
else //a[mid] < a[left]
{
if (a[left] < a[right])
{
return left;
}
else if (a[right] < a[mid])
{
return mid;
}
else
return right;
}
}
void QuickSort(int* a, int left, int right)//注意end到底是啥?如果是元素个数,下面的right要减1,如果是最后一个元素下标,end=right
{
//递归结束条件
if (left >= right)
return;
int begin = left, end = right;
//三数取中间值法,避免在已经有序的情况下快排过慢的情况
//int midi = GedMidi(a, left, right);
//Swap(&a[left], &a[midi]);
//或用随机数法,避免在已经有序的情况下快排过慢的情况,让随机数作keyi
//int randi = rand() % (right - left + 1);
//randi += left;
//Swap(&a[left], &a[randi]);
//上面的方法只是优化,可以先不看
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi]) //右找小
{
right--;
}
while (left < right && a[left] <= a[keyi]) //左找大
{
left++;
}
Swap(&a[left], &a[right]);
}
//相遇点和key交换
Swap(&a[keyi], &a[left]);
//第一次完成
//下面是递归部分
keyi = left;
Quicksort(arr, begin, key-1);
Quicksort(arr, key + 1,end)
}
其实快速排序还有两种方法,一种是挖坑法,它和上面的方法思路差不多,两种会一个即可
void Swap(int* a, int* b)
{
int t = *a;
*a = *b;
*b = t;
}
//挖坑法
void partQuicksort(int* a, int begin, int end)//end表示最后一个元素下标
{
if (begin >= end)
return;
int left = begin;
int right = end;
int key = a[begin];
int hole = begin;//坑
while (left < right)
{
while (left < right && a[right] >= key)
right--;
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
partQuicksort(a, begin, hole - 1);
partQuicksort(a, hole + 1, end);
}
另一种是前后指针版本
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
到目前为止,大家是不是觉得快排也就那样?现在我请你实现快排的非递归,请问你如何实现呢?
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);//初始化栈
//先入右,再入左
StackPush(&st, right);//入栈
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);//取栈顶
StackPop(&st);//删除栈顶元素
int end = StackTop(&st);
StackPop(&st);
//单趟
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
//先入右,再入左
//判断是否要入栈
if (keyi + 1 < end)
{
StackPush(&st, end);
StackPush(&st, keyi + 1);
}
if (keyi - 1 > begin)
{
StackPush(&st, keyi - 1);
StackPush(&st, begin);
}
}
StackDestroy(&st);//销毁栈
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
七、归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
void _MergeSort(int* a, int left, int right, int* tmp)
{
//结束条件判断
if (left == right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
//归并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
//比较,小的尾插tmp
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a+left, tmp+left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
归并排序的特性总结:
1. 时间复杂度:O(N*logN)
2. 空间复杂度:O(N*logN)
3. 稳定性:稳定
以上便是本篇博客的全部内容,感谢观看。