排序算法速递
选择排序与冒泡排序的罪与罚
首先这个问题的分析要看很多情况。关于选择排序,有些书说是稳定的,有些书说是不稳定的. 实际上是==不同的实现方法有不同的结果==. ==如果是在数组中交换,那么就有可能不稳定,如{5,5,2}== ==如果是链表或者开一个新的数组,那么又是稳定的了.== 所以,选择排序究竟属于稳定排序呢还是不稳定排序?
首先,选择排序是不稳定的。在《算法》第四版217页上作者已经说了,有很多办法可以将任意排序算法变成稳定的,但是,往往需要额外的时间或者空间。
一般提到排序算法时,大家往往会默认是数组实现,所以选择排序是不稳定的
分析
直接选择排序:直接选择排序的作法是:第一趟扫描所有数据,选择其中最小的一个与第一个数据互换;第二趟从第二个数据开始向后扫描,选择最小的与第二个数据互换;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
那 是不是稳定的呢,我们可以观察一次扫描中,要交换最小的一个与第一个数据,最小的这个我们可以保证它依然稳定,但我们能否保证第一个数据依然不破坏稳定 呢?答案是不可以,比如 2 3 2" 1,2与1交换后1 3 2" 2,这样的确就不是稳定的了。根据我们上面的分析,我们把1 3 2" 2作为一个新的序列我们可以发现,它应当与上面的2" 3 2 1执行到1 3 2" 2处的执行序列一样,严格说明了它肯定不稳定。因为要麽使2 3 2" 1不稳定,要麽使1 3 2" 2不稳定。
实际上如果用链表实现一个直接选择排序,这样它只改变了最小元素的位置,并没有交换,这样可以是稳定的,但一般的直接选择排序是指上面用交换的方法。
理解数组和链表
链表和数组是两种基本的数据结构,他们的区别在于数据在内存中的存储方式不同。
数组
数组在内存中是用一块连续的内存来存储数据的,数组中的每个数据地址是连续的。数组中的每个元素所占用的内存是相同的,所以,我们可以通过下标索引在常数数量级的时间内,迅速访问数组中的任何一个元素。但是要在数组中任意位置添加一个元素,就需要移动大量的元素,使得内存中空出一个位置来存放新插入的元素。同理,当删除一个元素的时候,也需要移动大量的元素,来使得删除元素以后的数组数据在内存中仍旧是连续的。
由此可见:当对于一组数据,读取操作频繁,写操作少的情况,应该使用数组数据结构。
链表
链表与数组相反,链表中的元素在内存中是随机存放的,链表中的每个元素都存放了当前元素的值以及下一个数据的内存地址指针。通过这个地址指针,使得随机存放的数据,得以相互之间连接起来,形成链表。如果我们需要访问链表中的某个元素的时候,需要从链表的第一个元素开始逐个遍历寻找,直到找到目标元素。这一点来说,远远不如数组的访问效率高。但是链表的优势在于,当我们想要插入或删除一个元素的时候,只需要处理一下要插入或删除的元素前后元素的地址指针,就可以完成。修改之后,不需要移动其他数据。
由此可见:当对于一组数据,增加删除元素操作频繁,读取操作少的时候,应该使用链表数据结构
二者区别
- 数组数据是连续的,一般需要预先设定数据长度,不能适应数据动态的增减,当数据增加是可能超过预设值,需要要重新分配内存,当数据减少时,预先申请的内存未使用,造成内存浪费。链表的数据因为是随机存储的,所以链表可以动态的分配内存,适应长度的动态变化;
- 数据的元素是存放在栈中的,链表的元素在堆中;
- 读取操作:数组时间复杂度为O(1),链表为O(n)
- 插入或删除操作:数据时间复杂度为O(n),链表为O(1)
选择排序
选择排序原理
每一轮比较找到一个极值(最大值或最小值)放到某一端,对剩下的数再找极值,直至比较结束。
开始学习算法的时候,对选择排序和冒泡有点混淆,这两种排序算法都是从待排序的列表中寻找最大或最小值,然后移动到最旁边。但这两种算法有些区别
- 冒泡排序的思想是:每一次排序过程,通过相邻元素的交换,将当前没有排好序中的最大(小)移到数组的最右(左)端。
- 选择排序的思想是:每一次排序过程,我们获取当前没有排好序中的最大(小)的元素和数组最右(左)端的元素交换,循环这个过程即可实现对整个数组排序。
基本实现
def selection_sort(arr):
length = len(arr)
if length < 2:
return arr
for out_idx, base in enumerate(arr):
min = out_idx
# 遍历剩下的数据
for inner_idx in range(out_idx+1, length):
if arr[inner_idx] < base:
min = inner_idx # 记录最小值的索引
if out_idx != min:
# 交换当前位置的数据和最小索引处的值
arr[out_idx], arr[min] = arr[min], arr[out_idx]
return arr
复杂度
最坏时间复杂度 О(n²)
最优时间复杂度 О(n²)
平均时间复杂度 | О(n²) |
优化实现:二元选择排序
每次确定两个数(最大值和最小值),减少迭代次数
def selection_sort(arr):
length = len(arr)
if length < 2:
return arr
for i in range(length//2):
min = i
max = -i - 1
max_origin = max
# 左右两边同时交叉遍历
for j in range(i+1, length-i):
if arr[j] < arr[min]:
min = j
if arr[-j - 1] > arr[max]:
max = -j - 1
if i != min:
arr[i], arr[min] = arr[min], arr[i]
# 如果此时恰好max也指向i,i被移动了,所以max也要相应的移动
if i == max or i == length + max:
max = min
if max_origin != max:
arr[max_origin], arr[max] = arr[max], arr[max_origin]
return arr
优化实现:等值情况
二元选择排序的时候,每一轮可以知道最大值和最小值,如果某一轮最大最小值都一样了,说明剩下的数字都是相等的,直接结束排序。
def selection_sort(arr):
length = len(arr)
if length < 2:
return arr
for i in range(length//2):
min = i
max = -i - 1
max_origin = max
for j in range(i+1, length-i):
if arr[j] < arr[min]:
min = j
if arr[-j - 1] > arr[max]:
max = -j - 1
# 如果最大最小值相等
if arr[min] == arr[max]:
break
if i != min:
arr[i], arr[min] = arr[min], arr[i]
if i == max or i == length + max:
max = min
if max_origin != max:
arr[max_origin], arr[max] = arr[max], arr[max_origin]
return arr
优化实现:等值情况优化
如果arr
= [2,1,1,1,1]
,找到最大值索引为-5, 最小值索引为1,上面代码会交换两次,第二次的两个1交换是多余的操作,所以添加一个判断,如果值相同则不交换。
def selection_sort(arr):
length = len(arr)
if length < 2:
return arr
for i in range(length//2):
min = i
max = -i - 1
max_origin = max
for j in range(i+1, length-i):
if arr[j] < arr[min]:
min = j
if arr[-j - 1] > arr[max]:
max = -j - 1
if arr[min] == arr[max]:
break
if i != min:
arr[i], arr[min] = arr[min], arr[i]
if i == max or i == length + max:
max = min
# 添加判断,如果两个索引处的数据相同,不交换
if max_origin != max and arr[max_origin] != arr[max]:
arr[max_origin], arr[max] = arr[max], arr[max_origin]
return arr