排序算法

## 插入排序

### 直接插入排序

原理:

  • 在未排序序列中,构建一个子排序序列,直至全部数据排序完成
  • 待排序的数,插入已经排序的序列中的合适位置
  • 增加一个哨兵,放入待比较值,让它和后面已经排好序的序列比较,找到合适的切入点

举例:

初始值:[1 9 8 5 6]

第一趟:[0 1 9 8 5 6] [9 1 9 8 5 6] -> [1 9 8 5 6]

如大家所看到的,和初始值必须,我在第一个位置上增加了一个数字0,这个数字0的位置我们称其为哨兵,接下来我们直接取初始值的第二个索引9和它前面一个索引比较(第一个索引相比较),哨兵9比1大,则不需要交换位置,本轮比较结束。

第二趟:[8 1 9 8 5 6] [8 1 ? 9 5 6] [8 1 8 9 5 6] -> [1 8 9 5 6]

拿到上一轮比赛结果,接下来我们直接取初始值的第三个索引8和它前面一个索引比较,本轮哨兵为8,而哨兵8比9小,因此,我们需要把9往右移动一位,此时哨兵被插入8和9交换位置索引变为第二个索引,此时依旧需要和前面的一个索引进行比较,很显然第一个索引为1,而哨兵为8,哨兵8比1大,则不需要交换位置,本轮比赛结束。(简单的说就是如果比本轮哨兵大就相互交换位置,如果比本轮哨兵小则本轮结束)

第三趟:[5 1 8 9 5 6] [5 1 8 ? 9 6] [5 1 ? 8 9 6] [5 1 5 8 9 6] -> [1 5 8 9 6]

拿到上一轮比赛结果,接下来我们直接去初始值的第四个索引5和它前面一个索引比较,本轮哨兵为5,而哨兵5比9小,因此他们需要相互交换位置,接着哨兵5再和它前面一个索引比较,哨兵5比8小,继续交换位置,然后继续和它前面一个索引比价,发现哨兵5比1大,终止本轮比赛。

第四趟:[6 1 5 8 9 6] [6 1 5 8 ? 9] [6 1 5 ? 8 9] [6 1 5 6 8 9] -> [1 5 6 8 9]

拿到上一轮的结果,继续重复插入排序比较,经过上面3次比较,想必大家也清楚它是如何排序的了,最终经过四趟比较,得到的排序是:1 5 6 8 9

思路:

  • 增加一个哨兵位,每躺将待比较的值放入其中
  • 哨兵依次和带比较数的前一个比较,大数向右移动,找到哨兵中的值要插入的位置
  • 每一轮结束,得到一个从开始到待比较数位置的一个有序序列
m_list = [
    [1,9,8,5,6,7,4,3,2],
    [1,2,3,4,5,6,7,7,9],
    [9,8,7,6,5,4,3,2,1],
    [1,1,1,1,1,1,1,1,2]
]

nums = [0] + m_list[2] # 添加哨兵位
print(nums[1:])

length = len(nums)
count_move = 0
for i in range(2, length): # 跳过哨兵和第一个元素,从第二个元素开始
    nums[0] = nums[i]      # 将带比较值放入哨兵位
    j = i - 1
    if nums[j] > nums[0]:  # 大数右移,找到插入位置
        while nums[j] > nums[0]:
            nums[j+1] = nums[j] # 右移
            j -= 1         # 继续向左比较
            count_move += 1
        nums[j+1] = nums[0] # 插入哨兵,j本是合适位置,但在退出循环之前被-1(19行),所以要加回来

print(nums[1:], '数据移动的次数:{}'.format(count_move), sep='\n')

总结:

  • 插入排序特点:
    • 最好情况,正好是升序排列,比较迭代n-1次
    • 最差情况,正好是降序排列,比较迭代1,2,...,n-1即 n(n-1)/2,同样也是数据移动的次数,数据移动非常之多
    • 使用两次嵌套循环,时间复杂度O(n^2)
    • 稳定排序算法
    • 用在小规模数据比较

什么是稳定排序算法

​ 待排序序列R中有两个元素相等,即R[i] = R[j],且i < j,那么排序之后这两个元素的先后顺序不变,这种排序算法就称做稳定排序。要判断哪些算法是稳定排序,拿[1, 1, 2]来测试就行

  • 待优化:
    如果比较操作耗时大的话,可以采用二分查找提高效率,即二分查找插入排序

# 选择排序

## 简单选择排序

简单选择排序属于选择排序的一种算法,原理是两两比较大小,找出极值(极大或者极小)放在固定的位置,这个固定的位置一般指的是某一端,结果可以是升序也可以是降序。

降序

n个数从左到右,开始遍历

第一轮:索引从0开始到n-1,两两相依比较,记录大值索引,此轮所有数比较完毕,将大数和索引0数交换,如果大数就是索引0,不交换。

第二轮:从索引1开始比较,找到最大值,将其和索引1位置交换,如果他就在索引1位置则位置不交换。

以此类推,每次左边都会固定下一个大数。

升序

和降序相反

简单选择排序的实现

m_list = [
    [3, 2, 7, 1, 1, 5, 8, 5, 2],
    [9, 1, 8, 8, 5, 3, 8, 8, 9],
    [7, 9, 8, 6, 3, 8, 9, 7, 1]
]

nums = m_list[2]
print(nums)

length = len(nums)
count_iter = 0 # 迭代的次数
count_swap = 0 # 元素位置交换的次数
for i in range(length):
    maxindex = i # 初始化最大值索引指针
    for j in range(i + 1, length):
        count_iter += 1
        if nums[j] > nums[maxindex]: # 最大值指针指向的值与无序区的元素一一比较,maxindex永远指向更大的值;遍历完一遍,就能找到无序区种的最大值了
            maxindex = j
    if i != maxindex: # 判定最大值指针是否移动过,没移动过就说明i 比无序区里面的数字都大了
        nums[i], nums[maxindex] = nums[maxindex], nums[i] # 将无序区选出来的最大值放在有序区的边界
        count_swap += 1
print(nums, count_iter, count_swap, sep='\n')


#运行结果
[7, 9, 8, 6, 3, 8, 9, 7, 1]
[9, 9, 8, 8, 7, 7, 6, 3, 1]
36
6

## 简单选择排序的优化--二元选择排序

同时选择固定左边最小值和右边最大值

优点:一次迭代,就找出最大&最小值,这样无序区范围再一次迭代就被再次缩小了 ,<font color=red>需要迭代的次数就少了</font>

m_list = [
    [3, 2, 7, 1, 1, 5, 8, 5, 2],
    [9, 1, 8, 8, 5, 3, 8, 8, 9],
    [7, 9, 8, 6, 3, 8, 9, 7, 1],
    [1, 1, 1, 1, 2, 1, 1, 1, 1]
]

nums = m_list[3]
print(nums)
length = len(nums)
count_iter = 0
count_swap = 0

for i in range(length // 2):
    '''
    为什么是迭代(length // 2) 次就能完成排序了?
    因为一次迭代就搞定了两个数,
    如果length是偶数,那么len//2次就能确定len个元素的顺序
    如果length是基数,那么len//2次就能确定(len-1)个元素的顺序,因为len//2是向下取整,len//2*2 + 1 = len,
    至于最后剩下那个数就是中间数,因为他左右两边的顺序都是好的了
    '''
    maxindex = i # 初始化最大值指针,指向无序区最左的元素,假定他为无序区里的最大值
    minindex = -i - 1 # 初始化最小值指针,指向无序区最右的元素,假定他为无序区里的最小值
    minorigin = length + minindex # 无序区初始化最小值的正索引,同时也是无序区最右的元素的正索引
    for j in range(i+1, length-i): # 遍历无序区除了一个极值外的数,range(i+1, minorigin+1) => range(i+1, length+minindex+1) => range(i+1, length+(-i - 1)+1)
        count_iter += 1
        if nums[j] > nums[maxindex]: # 找出无序区得最大值,方法是,两两比较,maxindex总是指向更大值的索引,比完一圈就找到最大值索引了
            maxindex = j
        if nums[-j - 1] < nums[minindex]: # 找出无序区得最小值,方法是,两两比较,minindex总是指向更小值的索引,比完一圈就找到最小值索引了
            minindex = -j - 1
    minindex = length + minindex # minindex在前面一直用的是负索引,这里转成正索引更方便
    if i != maxindex:
        nums[i], nums[maxindex] = nums[maxindex], nums[i] # 如果最大值就是无序区最左的元素,那就不用交换位置啦
        count_swap += 1
        if i == minindex: # 因为上面的swap操作使得元素的位置发生变化,如果被移位的i刚好是minindex,那么要更新minindex,因为我们追踪的是值,不是索引
            minindex = maxindex
    if minindex != minorigin: # 如果最小值就是无序区最右的元素,那就不用交换位置啦
        nums[minorigin], nums[minindex] = nums[minindex], nums[minorigin]
        count_swap += 1

print(nums, count_iter, count_swap, sep='\n')



#运行结果
[7, 9, 8, 6, 3, 8, 9, 7, 1]
[9, 9, 8, 8, 7, 7, 6, 3, 1]
20
6

可以优化的地方:

如果一次迭代中,找出来的极大值和极小值的值相等,说明比较序列元素全部相等,没有继续调整顺序的必要了。

m_list = [
    [3, 2, 7, 1, 1, 5, 8, 5, 2],
    [9, 1, 8, 8, 5, 3, 8, 8, 9],
    [7, 9, 8, 6, 3, 8, 9, 7, 1],
    [1, 1, 1, 1, 2, 1, 1, 1, 1]
]

nums = m_list[3]
print(nums)
length = len(nums)
count_iter = 0
count_swap = 0

for i in range(length // 2):
    maxindex = i 
    minindex = -i - 1 
    minorigin = length + minindex 
    for j in range(i+1, length-i):
        count_iter += 1
        if nums[j] > nums[maxindex]: 
            maxindex = j
        if nums[-j - 1] < nums[minindex]:
            minindex = -j - 1
    if nums[maxindex] == nums[minindex]: break  #优化之处
    minindex = length + minindex 
    if i != maxindex:
        nums[i], nums[maxindex] = nums[maxindex], nums[i] 
        count_swap += 1
        if i == minindex: 
            minindex = maxindex
    if minindex != minorigin: 
        nums[minorigin], nums[minindex] = nums[minindex], nums[minorigin]
        count_swap += 1

print(nums, count_iter, count_swap, sep='\n')

想[1, 1, 1, 1, 1, 1, 1, 1, 2]这种情况,第一轮找到的最大值索引是8,最小值索引是-2,如果按照上面的算法,会交换两次,两个最小值1交换就是无用功,所以增加一个判断。

说起来都是前面极大值交换位置引起的,minorigin索引指向的值被交换位置也不影响,因为minindex已经是无序区中最小的,换谁来当minorigin都一样,minindex都是最小,但是如果宝贝被换过的minorigin对应的值就有可能和minindex相同(原始的minorigin和minindex肯定是不同的,因为上面选minindex有做判断,minorigin被置换的话就不一样了),调换位置就是徒劳的。

m_list = [
    [3, 2, 7, 1, 1, 5, 8, 5, 2],
    [9, 1, 8, 8, 5, 3, 8, 8, 9],
    [7, 9, 8, 6, 3, 8, 9, 7, 1],
    [1, 1, 1, 1, 2, 1, 1, 1, 1]
]

nums = m_list[3]
print(nums)
length = len(nums)
count_iter = 0
count_swap = 0

for i in range(length // 2):
    maxindex = i 
    minindex = -i - 1 
    minorigin = length + minindex 
    for j in range(i+1, length-i):
        count_iter += 1
        if nums[j] > nums[maxindex]: 
            maxindex = j
        if nums[-j - 1] < nums[minindex]:
            minindex = -j - 1
    if nums[maxindex] == nums[minindex]: break  #优化1
    minindex = length + minindex 
    if i != maxindex:
        nums[i], nums[maxindex] = nums[maxindex], nums[i] 
        count_swap += 1
        if i == minindex: 
            minindex = maxindex
    if minindex != minorigin and nums[minorigin]!=nums[minindex]:  #优化2
        nums[minorigin], nums[minindex] = nums[minindex], nums[minorigin]
        count_swap += 1

print(nums, count_iter, count_swap, sep='\n')

## 总结

  • 简单选择排序需要数据一轮轮的比较,并在每一轮中发现机值
  • 没有办法知道当前轮是否已经达到排序要求,但是可以知道极值是否在目标索引上
  • 遍历次数:1,2,3,...,n-1之和,即n(n-1)/2
  • 时间复杂度O(n**2)
  • 减少了交换次数,提高了效率,性能率好过冒泡法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 快排上图中空间复杂度数据错误,应该是O(log n)。 插入,堆,归并,快排 n表示数据规模,k表示桶的个数。n:...
    hadoop_a9bb阅读 1,634评论 2 36
  • 十大经典排序算法 来源:https://github.com/wangguanfu/-Sorting-algori...
    星丶雲阅读 800评论 0 7
  • 简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间 时间复杂度计算时间复杂度的方法: 用常...
    Teci阅读 1,132评论 0 1
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! 1.冒泡排序(Bub...
    王饱饱阅读 1,817评论 0 7
  • 办法总比困难多。在大的环境无法改变时,在小的范围内,还是可以做到积极尝试。“世上无难事,只怕有心人。”逆风而行,总...
    叶雨1105阅读 131评论 0 1