手撕算法之冒泡选择插入希尔排序

Posted BMGx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕算法之冒泡选择插入希尔排序相关的知识,希望对你有一定的参考价值。


网上关于十大排序算法的文章很多,其实思想很好懂,但实际敲出来也不太容易,特别是想一次性敲对,难。个人觉得,难点或易错点主要在条件边界的控制和循环退出点的选择。不过熟能生巧,因为算法有点像恢复魔方,讲究套路。一个算法打发明起,到被广泛应用,越来越成熟优化的同时也意味着模式越来越固定。作为普通人,一辈子无缘发明新算法,能做到的就是反复操练会用代码实现现成的算法。


下面根据算法思路手打冒泡、选择、插入和希尔四种排序。它们的共性:①都是基于比较和交换。因此时间复杂度衡量上也主要考虑比较和交换操作。而比较交换带来的最大好处是单次操作仅申请常数量级的临时内存(空间复杂度O(1)),即不需要复制原始数据或申请相应规模的内存空间,从而实现了原地排序。②都是基于数组,所以可以通过下标快速访问,代码写起来简单。而数组原地排序最大的技巧就是双指针,通过快慢指针一边区分已排和未排,一边重复遍历比较大小。

Bubble_sort

 1# 冒泡排序代码实现@dra
2
3def bubble_sort(lst: list)->list:
4    n = len(lst)
5    if n <= 1:
6        return lst
7    for i in range(0, n):
8        mark = 0
9        for j in range(0, n-i-1):
10            if lst[j] > lst[j+1]:
11                tmp = lst[j+1]
12                lst[j+1] = lst[j]
13                lst[j] = tmp
14                mark = 1
15        if not mark:
16            break
17    return lst


上边代码的整体思路就是“每一次循环筛选出最大值,放到序列尾部,这样一共执行n层,总共的运算规模是n*n次”。

比较容易出错的地方是内层嵌套循环的end位是“n-i-1”,借助和外层循环的关联,达到了“气泡浮出水面”的动态下降。

可以优化的地方是增加局部变量mark,利用它的开关作用,在每一次冒泡过程中都检查一次序列有序度,如果已经有序则不用执行后边循环,直接跳出并返回。这样可以节省计算资源。

还有一个彻底优化的途径,即引入另一种算法——鸡尾酒排序。参见小灰的文章。

鸡尾酒排序的元素比较和交换过程是双向的。

小灰,公众号:程序员小灰

Selection_sort

 1# 选择排序实现代码@dra
2
3def selection_sort(lst: list)->list:
4    n = len(lst)
5    if n <= 1:
6        return lst
7    for i in range(n-1):
8        mini = i
9        for j in range(i+1, n):
10            if lst[j] < lst[mini]:
11                mini = j
12        if i != mini:
13            lst[i], lst[mini] = lst[mini], lst[i]
14    return lst


上边代码的整体思路是“通过遍历,动态确定最小数据的位置,并将最小值添加到有序部分末尾(实际上置换两个数位置)”。

比较容易出错的地方是外层循环的end位是“n-1”,而不是n。因为外层循环是用来初始化最小值的,它后边(也就是内层循环的start位之后)都是无序待排的,所以不能把外层循环end设为n。

选择排序的平均时间复杂度是O(n^2),可以使用快排或堆排优化,quick_sort和heap_sort的平均时间复杂度都是O(nlogn)。而快排核心的分组过程和堆排序核心的堆化过程都涉及“选择”。

所谓选择类排序和冒泡(及鸡尾酒)排序的区别是,前者是点对点交换,而后者是逐一连续交换。

Insertion_sort

 1# 插入排序代码实现@dra
2
3def insertion_sort(lst: list)->list:
4    n 
= len(lst)
5    if n <= 1:
6        return lst
7    for i in range(1, n):
8        value 
= lst[i]
9        j = i - 1
10        while j >= 0:
11            if lst[j] > value:
12                lst[j+1] = lst[j]
13                j -= 1
14            else:
15                break
16        lst[j+1] = value
17    return lst


上边代码的整体思路是“遍历数组,将当前位置的值插入到前边部分合适的位置”。要想实现原地排序,就需要整体向后一位搬移数据,从而错出一个空,然后把当前位数据插入空位。

比较容易出错的地方是内层循环退出条件是指针小于零,而不是小于等于零。另一处是,插空位指针是“j+1”,而不是j。

插入排序是一种直接插入,效率不高。希尔排序是对它的改进优化。

Shell_sort

 1# 希尔排序实现代码一@dra
2
3def shellsort(lst):
4    n 
= len(lst)
5    if n <= 1:
6        return lst
7    step = n//2
8    while step > 0:
9        for i in range (step, n):
10            value 
= lst[i]
11            j = i - step
12            while lst[j] > value and j >= 0:
13                lst[j+step] = lst[j]
14                j -= step
15                # temp = lst[i]
16                # lst[i] = lst[i-step]
17                # lst[i-step] = temp
18                # i -= step
19            lst[j+step] = value
20        step //= 2
21    return lst

1# 希尔排序实现代码二@dra
2
3def shell_sort(lst):
4    n 
= len(lst)
5    if n <= 1:
6        return lst
7    step = n//2
8    while step > 0:
9        for i in range (step, n):
10            while lst[i-step] > lst[i] and i >
= step:
11                temp = lst[i]
12                lst[i] = lst[i-step]
13                lst[i-step] = temp
14                i -= step
15        step //= 2
16    return lst


上边代码是最普通的一种希尔排序,其整体思路是“通过增量步长对序列分组,各组组内排序;然后不断减半增量步长,重复前边过程”。各组组内排序时用到的方法就是直接插入,可以说直接插入排序实际上是希尔排序的一种特例,就是步长step为1的情形。

比较容易出错的地方是外层循环的指针区间一定是(step,n),而不能是(0,step)。因为,如果n为偶数,两者效果确实相等;但n若为奇数,(0,step)会遗漏序列最后一个数。

希尔排序的优化基本上就是对增量数列的优化。增量步长使用1/(2^k)作系数是最常见的,它的平均时间复杂度是O(n^1.5)。如果使用Sedgewick大神的算法,将使复杂度缩减为O(n^1.3),可见下面这篇文章。

目前已知的最好步长序列,最坏情况时间复杂度为O(n^[4/3]),是在1986年由Robert Sedgewick提出的。1,5,19,41,109...

http://www.cocoachina.com/articles/121517?filter=ios

你甚至还可以见到各种稀奇古怪的递减增量数列,如下:

增量序列在希尔排序中是很重要的。一般好的增量序列都有两个共同的特征:1. 最后一个增量必须为1,保证最后一趟是一次普通的插入排序;2. 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况

倪升武,公众号:武哥聊编程

综上,可以看出增量数列的选择将决定希尔排序的效率。那么,怎样的增量方式是更加有效的?规律是:要尽量保证数列中每个值之间彼此“互质”,即是说两两之间没有除1外的公约数。(我一直觉得算法的底层是数学,普通算法常用到数论、拓扑,机器学习算法常用到线性代数、贝叶斯概率,等等。所以,数学能力决定编程上限。 不过多说一句,计算机之于数学同物理之于数学不太一样。纵观物理学史或是力学史,充满了数学定理的诞生记录,因为物理学研究为数学工具的拓展提供了土壤,或者也可以理解为物理学探索天体、探索宇宙、探索宏微观物质世界,而数学规律则存在于大自然背后,等待被发现。计算机则偏应用,拿来已有的数学公式或理论作算法支撑,并封装成编译器或虚拟机的api供上层使用。当然计算机的超强算力使穷举成为可能,可以帮助完成某些数学证明。

排序算法分析

名称 时间复杂度 空间复杂度 稳定性
冒泡 O(n^2) 原地排序 稳定
选择 O(n^2) 原地排序 不稳定
插入 O(n^2) 原地排序 稳定
希尔 O(n^1.3) 原地排序 不稳定


This browser does not support music or audio playback. Please play it in WeChat or another browser.

以上是关于手撕算法之冒泡选择插入希尔排序的主要内容,如果未能解决你的问题,请参考以下文章

[新星计划] Python手撕代码 | 十大经典排序算法

[ 数据结构 -- 手撕排序算法第一篇 ] 插入排序

万字手撕七大排序(代码+动图演示)

[ 数据结构 -- 手撕排序算法第四篇 ] 选择排序

算法_基本排序算法之冒泡排序,选择排序,插入排序和希尔排序

排序算法之冒泡选择插入排序(Java)