数据结构|排序算法学习(1)(直接插入排序,希尔排序,选择排序,堆排序,冒泡排序)
文章目录
- 直接插入排序
-
- 代码实现
- 复杂度的计算
- 希尔排序
-
- 希尔排序的预排序
- 代码实现
- 选择排序
-
- 代码实现
- 堆排序
- 冒泡排序
-
- 代码实现
注:以下排序都是以排升序为例。直接插入排序
文章图片
直接插入排序的主要思路就是:
1.首先默认第一个元素是有序的。
2.然后将其下一个元素作为待排序的元素,插入到前面有序序列的相应位置。至于插入的过程,如果遇到比待排序大的元素,则这个元素后移,直到遇到比其小的元素,然后将待插入元素放入其前一个位置。
文章图片
代码实现
void Insertsort(int *a, int sz)
{ int i = 0;
for (i = 0;
i < sz-1;
i++)
{//用j来记录i的位置,这样可以省得到时候有些情况i从头或接近从头开始走
int j = i;
int temp = a[i + 1];
while (i >= 0)
{if (temp
注:绝大部分的直接插入排序算法其实是没有在代码中记录i的位置的,其实记录了i的位置可以提高执行效率,因为这样可以省得到时候有些情况i从头或接近从头开始走。复杂度的计算
文章图片
文章图片
在代码中,创建的临时变量个数是常数个,所以空间复杂度为O(1)。希尔排序
在上面的直接插入排序中我们会发现:
1.普通插入排序的时间复杂度最坏情况下为O(N2),此时待排序列为逆序,或者说接近逆序。
2.普通插入排序的时间复杂度最好情况下为O(N),此时待排序列为升序,或者说接近升序。
于是希尔这个科学家就想:若是能先将待排序列进行一次预排序,使待排序列接近有序(接近我们想要的顺序),然后再对该序列进行一次直接插入排序。因为此时直接插入排序的时间复杂度为O(N),那么只要控制预排序阶段的时间复杂度不超过O(N^2),那么整体的时间复杂度就比直接插入排序的时间复杂度低了。希尔排序的预排序
希尔排序,又称缩小增量法。那么如何缩小增量了?
1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作 (一般情况下我们取得第一增量大小是序列长度的一半)
2.当增量的大小减到1时,其实此时预排序已经完成了,这时已经达到我们一开始要的目的:让序列接近我们要的顺序。
3.最后对整个序列进行直接插入排序即可。
举个例子来看下。
文章图片
【数据结构|排序算法学习(1)(直接插入排序,希尔排序,选择排序,堆排序,冒泡排序)】在这个序列中,gap=3时其实就是对原序列进行了预排序。使其接近我们要的升序,方便之后的直接排序。
代码实现注:希尔排序和直接插入排序是稳定排序:我们可以发现,在希尔排序的过程中,序列中两个9的相对位置没有发生改变,具有这样特性的排序,我们称之为稳定排序。
void Shellsort(int *a, int sz)
{ int i = 0;
int gap = sz / 2;
while (gap >= 1)
{for (i = 0;
i < sz-gap;
i++)
{int j = i;
int end = i;
int temp = a[i + gap];
//这儿一定要记录下那个位置的数据,而不是写成temp=i+gap记录下标的形式,因为之后那个记录的下标的位置的数据可能被前面的数据前移占据了。
while (end >= 0)
{if (temp < a[end])
{//a[temp] = a[end];
因为下面要用到a[end+gap]=a[temp],所以这儿不能写成a[temp]
a[end + gap] = a[end];
end -= gap;
}
else
{break;
}
}
a[end + gap] = temp;
i = j;
}
gap = gap / 2;
}
}
选择排序选取这种增量方法的时间复杂度:O(nlogn),具体这个我是这么理解的,首先预排序阶段,你每次都将gap除以2,其实也就是高度次:logn,预处理中的交换次数可以看成常量,然后预处理完,这时已经接近最好情况的直接排序了,时间复杂度为O(n),所以最终的时间复杂度为O(nlogn),具体的证明过程等后序会补充更新。
空间复杂度:这里我们只定义了常数个常量,故空间复杂度为O(1)。
文章图片
代码实现选择排序
1.首先从整个数组中选出最小值,记录下最小值位置的下标与第一个位置交换,这样相当于最小值就到了头上。
2.此时将除最小值之后的元素再来选出次小的数,与第二个位置交换,之后迭代这个过程,最终就可以排成升序。
void chosesort(int *a, int sz)
{ int i = 0;
for (i = 0;
i < sz;
i++)
{int min = i;
for (int j = i;
j < sz;
j++)
{if (a[j] < a[min])
{min = j;
}
}
swap(&a[i], &a[min]);
}
}
时间复杂度:最坏的情况下是一个等差数列为O(n^2)
空间复杂度:创建了常数个变量,为O(1)
实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
void chosesort(int* a, int sz)
{ int start = 0;
int end = sz - 1;
while (start < end)
{int min = start;
int max = start;
for (int j=start;
j <= end;
j++)
{if (a[j] < a[min])
{min = j;
}
if (a[j]>a[max])
{max = j;
}
}
swap(&a[start], &a[min]);
//万一最大的值在最左边,这样max的位置的值就被最小值覆盖了,如果没有这个处理,会导致下面交换时出错。
if (max == start)
{max = min;
}
swap(&a[end], &a[max]);
start++;
end--;
}
}
堆排序注:这里要小心一种极端情况:万一最大的值在最左边,这样max的位置的值就被最小值覆盖了,如果没有if这个处理,会导致下面交换时出错。
博主之前专门写了一篇关于堆排序以及堆的文章,由于是最近不久写的,这里就不再赘述了,堆排序以及堆相关的知识冒泡排序
文章图片
相信冒泡排序是很多小伙伴第一个知道的排序算法。代码实现
其实它的思想正和他的取名一样,它就是每趟排序冒出一个最大(最小)值。那么它是如何冒的了?很简单,相邻两个元素比较,前一个比后一个大,则交换。
void Bubblesort(int *a, int sz)
{ int i = 0;
int flag = 0;
//记录原序列是否有序
for (i = 0;
i < sz - 1;
i++)
{int j = 0;
for (j = 0;
j < sz - i - 1;
j++)
{if (a[j]>a[j + 1])
{swap(&a[j], &a[j + 1]);
flag = 1;
//交换过了,说明原序列无序
}
}
if (flag == 0)//原序列一次交换都没有发生,说明原序列本来就有序,无需在继续进行
{break;
}
}
}
注:这里可以自己定义一个标识符flag来记录原序列是否发生过交换,如果没有发生过,则说明原序列本来就有序,无需再进行之后的趟数。
推荐阅读
- 画解算法(1.|画解算法:1. 两数之和)
- Guava|Guava RateLimiter与限流算法
- 一个选择排序算法
- SG平滑轨迹算法的原理和实现
- 《算法》-图[有向图]
- 排序(归并排序)
- LeetCode算法题-11.|LeetCode算法题-11. 盛最多水的容器(Swift)
- 虚拟DOM-Diff算法详解
- 《数据结构与算法之美》——队列
- 【图解】9张图彻底搞懂堆排序