最近在复习算法,看了好多的博文,写的都不是太明白。这里我打算进行一次性总结,方便自己查看,也方便他人学习。正所谓赠人玫瑰手有余香
评价一个算法无外乎从三个方面出发,
1,时间复杂度
2,空间复杂度
3,稳定性
本次总结六个排序算法,本文将从代码,时间复杂度,空间复杂度,稳定性以及改进这四个方面进行总结。本文采用C++
- 冒泡排序
- 插入排序
- 选择排序
- 归并排序
- 快速排序
- 堆排序
首先是一个Swap()函数,好多方法都会用到这个交换函数,直接写在最前面。
void Swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
一,冒泡排序
经典冒泡排序的代码:
void Bubble(int* arry,int length) {
if (arry == NULL || length < 2) { //判断输入是否合法
return;
}
for(int i=length-1;i>0;i--){ //比较多少轮
for (int j = 0;j < i;j++) { //每一轮比较多少次
if (arry[j] > arry[j + 1]) {
Swap(&arry[j], &arry[j + 1]);
}
}
}
}
时间复杂度:
冒泡排序的时间复杂度是严格的O()的,这一点从但代码段就可以看出。因为冒泡排序是跟数据状况没有关系的,无论你的数据状况是否有序,只要代码中i变量的值不越界,他就会一直比下去,直到i越野跳出循环。
关于O()的推到
假设有n个元素的数组,那么第一轮需要比较n-1次,那么第二轮需要比较n-2次,第三轮需要比较n-3次,,,,,,往下推,直到最后那么总共需要比较的次数的表达式为:(n-1)+(n-2)+(n-3)+...+2+1;这是一个等差数列,根据等差数列的求和公式可得出表达式为:;根据时间复杂度的求法(不包括这个函数的低阶项和首项系数——百度一下时间复杂度),最后化简为O()。冒泡排序,选择排序,插入排序的时间复杂度推到均是如此。
空间复杂度:
冒泡排序没有用到额外的辅助空间,只用到了几个少数变量,因此空间复杂度为O(1)。
稳定性:
排序的稳定性就是这个算法是否改变原数据中相同元素的位置。
冒泡排序是可以做到稳定性的,在代码的第二个if语句中如果arry[ j ]<=arry[ j+1 ],跳出if语句,就不会发生交换。
如果上面的第二个if语句改为arry[ j ]>=arry[ j+1 ],那么这段代码就做不到稳定性了。
优化:
对于一组数据{1,2,3,5,4},使用上面的排序算法,第一轮交换后数据就变成了{1,2,3,4,5},此时数据已经有序了。但是由于冒泡排序是跟数据的状况没关系的,只跟是否完全部轮的比较有关。那么接下来的第二轮,第三轮还是会不停的比较下去,虽然并不会发生交换行为,但是浪费了比较次数。
因此我们在代码里加入一个bool类型的变量来监控在每一轮的比较中是否发生了交换,一旦其中的的一轮比较没有发生交换行为,此时数组就已经有序了。
改进后冒泡排序的代码
void Bubble(int* arry,int length) {
if (arry == NULL || length < 2) { //判断输入是否合法
return;
}
bool _isChange;
for(int i=length-1;i>0;i--){ //比较多少轮
_isChange = false;
for (int j = 0;j < i;j++) { //每一轮比较多少次
if (arry[j] > arry[j + 1]) {
Swap(&arry[j], &arry[j + 1]);
_isChange = true;
}
}
if (!_isChange) return;
}
}
改进后的时间复杂度可以达到O(n),空间复杂度为o(1);
二,插入排序
经典插入排序的代码
void Insert(int* arry, int length) {
if (arry == NULL || length < 2) {
return;
}
for (int i = 1;i < length;i++) { //把[1,length)看成无序区,[0]为有序区
for (int j = i - 1;j >= 0 && arry[j] > arry[j + 1];j--) { //从无序取划进来一个数到有序区,然后在有序区中从后向前调整
Swap(&arry[j],&arry[j + 1]);
}
}
}
[点击并拖拽以移动]
时间复杂度:
经典的插入排序的时间复杂度是O(),(这个算法的最好时间复杂度为O(n)也就是当一个数组已经有序的情况下)
空间复杂度:
只在两个for循环中使用了几个变量,所以空间复杂度为O(1);
稳定性:
同冒泡排序的原理一样,可以做到稳定排序。
优化:
没想好
三,选择排序
经典的选择排序代码
void SelectSort(int* arry, int length) {
if (arry == NULL || length < 2) {
return;
}
for (int i = 0;i < length - 1;i++) {
int index = i; //每一轮排序都把这一轮中第一个元素当成最小
for (int j = i + 1;j < length;j++) { //arry[i]为每一轮的第一个,那么arry[j]就是第二个
index = arry[j] < arry[index] ? j : index;
}
Swap(&arry[i], &arry[index]);
}
}
时间复杂度:
经典的选择排序的时间复杂度是严格的O();
空间复杂度:
O(1);
稳定性:
做不到稳定性,下面给出了一组数据,可以照着代码推一下。
直接选择排序: A = {4, 4, 2, 5},排序后 A = {2,4, 4, 5}
有的人可能会抬杠说:把<号改成=<不就行了?
index = arry[j] < arry[index] ? j : index;
那你试试呗。
优化:
空
归并排序
经典归并排序代码
void Merge(int* arry,int start,int mid,int end) {
int length = end - start + 1;
int* newarry = new int[length];
int i = start;
int j = mid + 1;
int index = 0;
while (i<=mid&&j<=end)
{
newarry[index++] = arry[i] < arry[j] ? arry[i++] : arry[j++];
}
while (i<=mid)
{
newarry[index++] = arry[i++];
}
while (j<=end)
{
newarry[index++] = arry[j++];
}
for (i = 0;i < length;i++) {
arry[start + i] = newarry[i];
}
}
void MergeSort(int* arry, int start, int end) {
if (start == end) {
return;
}
int mid =(start+end)/2 /*start + ((end - start) >> 1)*/;//求中点的位置这个写法能够防止下表越界,而且位运算比算术运算效率快
MergeSort(arry, start, mid);
MergeSort(arry, mid + 1, end);
Merge(arry, start, mid, end);
}
void MergeSort(int* arry, int length) {
if (arry == NULL || length < 2) {
return;
}
MergeSort(arry, 0, length - 1);
}
时间复杂度
归并排序的时间复杂度分为两部分,一部分是比较和交换,另一部分是新数组给老数组赋值。
根据master公式(是用来利用分治策略来解决问题经常使用的时间复杂度的分析方法)
n为样本量;
a为一个递归子过程拆分的次数
b表示每次递归是原来的1/b之一个规模
n/b是子过程的样本量
是除去递归操作外的