1. 写在前面

这篇文章复习排序算法, 排序算法的重要性也是不言而喻, 并且面试的时候经常会问到第K大,第K小,前K大,前K小的问题, 这往往都是排序算法的解决范畴。 在实际问题中,排序算法也是应用非常广泛,所以看看排序这块到底是怎么玩的。 在整理具体的题目之前, 我必须先把基础知识给整理下来, 就是那十大经典的排序算法了,包括思路和代码框架。 这里面有几个巨重要, 比如快排, 归并,堆这哥仨。

  • 冒泡排序和插入排序是最基础的,面试官有时候喜欢拿它们来考察你的基础知识,并且看看你能不能快速地写出没有 bug 的代码。
  • 归并排序、快速排序和拓扑排序的思想是解决绝大部分涉及排序问题的关键。
  • 堆排序和桶排序,也非常重要,尤其是桶排序,在一定的场合中(例如知道所有元素出现的范围时),能在线性的时间复杂度里解决战斗,掌握好它的解题思想能开阔解题思路。

如果仅仅整理,也没啥意思,这里就拿个题来串联了。 LeetCode912: 排序数组, 下面就用各种排序方法来试一下这个题目, 各种算法原理这里不整理,大体过下思路,然后默写代码框架, 下面给出的链接里面是其他大佬对部分重点排序过程的详细解释和十大经典排序算法的动画演示, 真心感觉共享的时代是多么的好!

1.1 冒泡排序

数据结构学习排序算法的开始就是它,老生常谈了, 两层for循环, 外层循环控制轮数, 内存循环遍历数组实施具体排序过程。

核心: 每一轮都是两两相邻元素比较, 一旦逆序,立马交换, 这样每一轮结束,都会有一个数归位到最后的位置上去

优化点: 对于每一轮,都设置一个hasChange来表示这一轮是否发生了交换, 如果没有发生交换的话,说明已经排好序了,停掉外层循环就行了,比如从小到大排序, 一开始给定的就是排序好了的, 那还循环啥? 停掉就完事了。

# 冒泡排序
def BubbleSort(self, nums):# 外层控制轮数for i in range(len(nums)-1):# 定义一个布尔遍历,标记是否在每一轮发生过交换,如果没有交换了,说明排好了, 停止hasChange = False# 内层遍历数组,相邻元素一旦逆序,立马交换for j in range(len(nums)-1-i):if nums[j] > nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]hasChange = True# 当前轮结束,看是否发生了交换,一旦没有,退掉if not hasChange: breakreturn nums

时间复杂度O(n2)O(n^2)O(n2), 空间复杂度O(1)O(1)O(1), 稳定的排序算法(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变),之所以能稳定,就在于那个一旦逆序,才交换上, 两个相等的数,不算逆序,不在交换范畴。

这个题目,冒泡会超时。

1.2 插入排序

核心:不断地将尚未排好序的数插入到已经排好序的部分。在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来说,经过每一轮的排序处理后,数组前端的数都是排好序的。

两层for循环, 外层循环是遍历未排好序的每个元素, 内层循环,在已排好序的里面给当前的元素找合适的插入位置。

# 插入排序
def InsertSort(self, nums):# 在首位置设置一个哨兵,暂时存放待插入的元素, 并且防止下标越界nums = [0] + nums# 假设位置1的数是已排好序的, 2到后面的是未排好序的, 那么从2开始,把元素往前面插for i in range(2, len(nums)):# 逆序的才进行调整if nums[i] < nums[i-1]:# 当前元素存入哨兵位置,以防前面元素后移插入覆盖掉nums[0] = nums[i]# 第二层循环从前面已经排好的里面寻找插入位置j = i-1while nums[0] < nums[j]:nums[j+1] = nums[j]j -= 1# j+1位置就是要插入的位置nums[j+1] = nums[0]# 去掉哨兵return nums[1:]

有了哨兵之后,这个题会简单一些了。时间复杂度O(n2)O(n^2)O(n2), 空间复杂度O(1)O(1)O(1), 稳定的排序算法。 之所以稳定,是因为这里也是但前面的数严格比后面的大的时候,才后移插入。这时候,相同的数,位于前面的位置上的会先插进去, 而后面这个插的时候,也是插到它后面。

当然,这个在这里也超时了。

1.3 选择排序

核心:每一轮选取未排定的部分中最小的部分交换到未排定部分的最开头,经过若干个步骤,就能排定整个数组

依然是两层for循环, 外层循环负责一个排好序的数组, 内层循环负责找未排好序的里面最小值,然后放到排好序的那里面。

# 选择排序
def SelectSort(self, nums):# 外层循环维护排好序的数组for i in range(len(nums)-1):min_index = i# 内层循环从这个的后面找当前最小元素for j in range(i+1, len(nums)):if nums[j] < nums[min_index]:min_index = jif min_index != i:nums[min_index], nums[i] = nums[i], nums[min_index]return nums

时间复杂度:O(n2)O(n^2)O(n2) , 空间复杂度:O(1)O(1)O(1), 非稳定排序, 因为这个它是会置换的, 有可能会把原先排在前面的那个置换到后面去。比如7, 7', 4, 8, 2, 第一个7一下子到了末尾了, 再排的话,就到了7'后面。

同样,会超时, 这个题肯定是让写比较优的算法。下面这几个就可以了, 不过上面的这几个是基础,稍微串了下。下面就是重头戏了。

1.4 归并排序 巨重要

核心是分治,就是把一个复杂的问题分成两个或多个相同或相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致

一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,才开始排序。排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。

# 归并排序 这个就是合并了两个有序数组的函数
def Merge(self, left, right):m, n = len(left), len(right)merge_arr = [0 for _ in range(m+n)]# 从后往前遍历,进行合并while m and n:if left[m-1] > right[n-1]:merge_arr[m+n-1] = left[m-1]m -= 1else:merge_arr[m+n-1] = right[n-1]n -= 1if m:merge_arr[:m] = left[:m]if n:merge_arr[:n] = right[:n]return merge_arrdef MergeSort(self, nums):# 递归结束  只有1个元素了 返回if len(nums) <= 1: return nums# 数组一分为2mid = len(nums) // 2# 把左边和右边的部分排好序 [5], [2,3,1]left = self.MergeSort(nums[:mid])right = self.MergeSort(nums[mid:])# 然后把这两部分合并起来, 这个就是两个有序数组合并成一个有序数组return self.Merge(left, right)

时间复杂度:O(nlog2n)O(nlog_2n)O(nlog2​n) , 空间复杂度:O(n)O(n)O(n) , 稳定排序, 这个能过。这个地方写个时间复杂度的推导吧,可能会考到。

1.5 快速排序 重点知识

从数组中选择一个元素,我们把这个元素称之为中轴元素,把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。

从中轴元素那里开始把大的数组切割成两个小的数组(两个数组都不包含中轴元素),接着我们通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。

 # 快速排序
def QuickSort(self, nums):def quick(left, right):   # [left, right]# 如果只有一个元素了,返回if left >= right:  return nums# 中轴选定第一个元素pivot, i, j = left, left, rightwhile i < j:# 从右边找比中轴小的数while i < j and nums[j] > nums[pivot]:j -= 1# 从左边开始找比中轴数大的数while i < j and nums[i] <= nums[pivot]:i += 1# 交换两个元素的位置,使得左边的元素不大于pivot,右边的不小于pivotnums[i], nums[j] = nums[j], nums[i]# 把中轴放到有序的位置上, 使得左边的元素小于中轴,右边的元素大于中轴nums[pivot], nums[j] = nums[j], nums[pivot]# 中轴左边和中轴右边快排quick(left, j-1)quick(j+1, right)return numsreturn quick(0, len(nums)-1)

时间复杂度:O(nlogn)O(nlogn)O(nlogn) , 空间复杂度:O(1)O(1)O(1) , 不稳定排序,因为上面j位置和中值交换的时候,有可能是两个一样的数, 这样一换,就把后面的换到前面去了。 这个能过。

1.6 堆排序 重点

堆的特点就是堆顶的元素是一个最值,大顶堆的堆顶是最大值,小顶堆则是最小值。

堆排序就是把堆顶的元素与最后一个元素交换,交换之后破坏了堆的特性,我们再把堆中剩余的元素再次构成一个大顶堆,然后再把堆顶元素与最后第二个元素交换…如此往复下去,等到剩余的元素只有一个的时候,此时的数组就是有序的了。

  • 大顶堆: 每个节点的值都大于或等于其子节点的值,在排序算法中用于升序
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序

基本思路:

  1. 创建一个堆
    buildMaxHeap:把一个无序的数组,排成最大堆(即父节点总比子节点小),这个建堆的过程称之为 heapify。 从最后一个非叶节点开始向前遍历, 即从i=(lastNode−1)//2=(len(arr)−2)//2i=(lastNode-1)//2=(len(arr)-2)//2i=(lastNode−1)//2=(len(arr)−2)//2个节点开始逆序从下往上进行heapify操作。比如下图从i=3i=3i=3开始, 其实i=(len(arr)−2)//2i=(len(arr)-2)//2i=(len(arr)−2)//2也是可以的,就是从下图的i=4i=4i=4开始, 第一次操作会由于越界直接略过
  2. 把堆首(最大值)和堆尾互换
    HeapSort: 第一步建完堆以后,最大的数在最顶上的节点(i=0i=0i=0)。从最后一个节点开始,从下往上操作

    • 把最后一个节点和最顶上的节点交换(最大的数从堆顶i=0i=0i=0跑到最后i=(len(arr)−1)i=(len(arr)-1)i=(len(arr)−1)
    • 拿出最后一个元素(从原堆中砍断出来), 这就是最大的元素,这样堆就被打乱了
    • 重新恢复最大堆,对最顶上的元素再做heapify, 下面还是乱的,递归进行,即可恢复

代码如下:

# 堆排序
def heapify(self, tree, n, i):"""把父节点和最大子节点交换:param n: 总共的节点个数:param i: 元素的索引,对第i个元素做heapify"""# if i >= n:      # 递归出口 0 <= i <= (n-1),但是由于并不会越界,此处不需要递归出口#     returnc1 = 2 * i + 1   # 左边子节点c2 = 2 * i + 2   # 右边子节点# 找出父节点&两个子节点中最大的节点max_ele = i         # 假设父节点为最大值if c1 < n and tree[c1] > tree[max_ele]: max_ele = c1if c2 < n and tree[c2] > tree[max_ele]: max_ele = c2if max_ele != i:tree[max_ele], tree[i] = tree[i], tree[max_ele]# 递归 如果原先是max_ele节点,这里会被调换过,就是比较大的值被拿到父节点,换来了一个比较小的数,需要往下递归下去self.heapify(tree, n, max_ele)    def build_heap(self, tree, n):"""建堆;:param n: 数组的长度或者节点的个数"""last_node = n - 1         # 最后一个节点parent = (last_node - 1) // 2      # 最后一个节点对应的父节点# 从最后一个非叶子节点逆序遍历, 调整建堆for i in range(parent, -1, -1):self.heapify(tree, n, i)def HeapSort(self, tree, n):"""堆排序"""self.build_heap(tree, n)for i in range(n-1, -1, -1):# 最后一个节点和最顶上的节点交换tree[0], tree[i] = tree[i], tree[0]# 拿出(砍断)最后一个节点(上一步交换完后最大的数已经在最后了),而后从最顶上节点开始 heapify,又往下递归,直到形成稳定堆# 此处 heapify的参数是 i 而不是 n,因为不断砍断以后,整个数组长度从 n 变成了 iself.heapify(tree, i, 0)return tree

关于,堆排序,还得整理点厉害的知识,就是python里面的heapq模块。该模块提供了堆排序算法的实现。 这里整理常见的基本使用: 重点

  1. 建堆
    两种方式: 一种是使用空列表,然后heapq.heappush()函数把值加入,另外一种是heap.heapify(list)转换列表成堆。

    import heapqnums = [2, 3, 5, 1, 54, 23, 132]# 第一种方式
    heap = []
    for num in nums:heapq.heappush(heap, num)# 第二种方式
    heapq.heapify(nums)# 堆排序结果
    [heapq.heappop(heap) for _ in range(len(nums))]
    
  2. 访问堆内容
    堆建好之后,可以通过heapq.heappop()函数弹出堆中最小值。

    import heapqnums = [2, 43, 45, 23, 12]
    heapq.heapify(nums)print(heapq.heappop(nums))  # 2
    

    如果需要删除堆中最小元素并加入一个元素,可以使用heapq.heaprepalce() 函数, 这个在下面一个题目中会用到

    import heapqnums = [1, 2, 4, 5, 3]
    heapq.heapify(nums)heapq.heapreplace(nums, 23)print([heapq.heappop(nums) for _ in range(len(nums))])
    # out: [2, 3, 4, 5, 23]
    

    如果获取前kkk个最大或者最小值, 则可以使用heapq.nlargest()heapq.nsmallest() 函数

    import heapqnums = [1, 3, 4, 5, 2]
    print(heapq.nlargest(3, nums))    # [5, 4, 3]
    print(heapq.nsmallest(3, nums))   # [1, 2, 3]
    

    这两个函数还接收一个key参数,可以传入字典,来指定不同的属性获取最大最小。这个在别的地方是非常实用的。

    import heapq
    from pprint import pprint
    portfolio = [{'name': 'IBM', 'shares': 100, 'price': 91.1},{'name': 'AAPL', 'shares': 50, 'price': 543.22},{'name': 'FB', 'shares': 200, 'price': 21.09},{'name': 'HPQ', 'shares': 35, 'price': 31.75},{'name': 'YHOO', 'shares': 45, 'price': 16.35},{'name': 'ACME', 'shares': 75, 'price': 115.65}
    ]
    cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
    expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
    pprint(cheap)
    pprint(expensive)"""
    输出:
    [{'name': 'YHOO', 'price': 16.35, 'shares': 45},{'name': 'FB', 'price': 21.09, 'shares': 200},{'name': 'HPQ', 'price': 31.75, 'shares': 35}]
    [{'name': 'AAPL', 'price': 543.22, 'shares': 50},{'name': 'ACME', 'price': 115.65, 'shares': 75},{'name': 'IBM', 'price': 91.1, 'shares': 100}]
    """
    

    堆的值可以是元组类型, 可以实现对带权值的元素进行排序。

    >>> h = []
    >>> heappush(h, (5, 'write code'))
    >>> heappush(h, (7, 'release product'))
    >>> heappush(h, (1, 'write spec'))
    >>> heappush(h, (3, 'create tests'))
    >>> heappop(h)
    (1, 'write spec')
    
  3. 合并两个有序列表 heapq.merge(*iterables)方法
    这个方法用于合并多个排序后的序列成一个排序后的序列,返回排序后的值的迭代器

    import heapqnums1 = [32, 3, 5, 34, 54, 23, 132]
    nums2 = [23, 2, 12, 656, 324, 23, 54]nums1.sort()
    nums2.sort()res = heapq.merge(num1, num2)
    print(list(res))
    

2. 题目梳理与思路总结

  • 面试题17.14: 最小的K个数:这个题目是快排的变形题目, 最小的K个数我们没有必要用快排先把所有数排好,然后再返回前K个,因为快排有个非常好的特点,就是中轴位置, 排一次, 中轴位置的数归位,那么中轴位置左边的数都比中轴小,中轴右边的数都比中轴大,当中轴在K位置的时候, 最小的K个数就是中轴左边的那些数了。所以这个题,只需要在快排的基础上,判断下中轴位置,根据这个进行递归左半边还是右半边即可。


    这个题还得记录一个堆排序的方式,因为快速排序也有局限性

    1. 算法修改原数组, 如果规定原数组不能修改的话,还需要拷贝一份数组,空间复杂度上去了
    2. 算法需要保存所有数据。 不适合海量数据求K个最小
    3. 快速排序找的最小的K个数并不是排好序的,如果题目要求还得返回从小到大排好序的最下K个数,那这个就不适用了。

    堆排序的优势恰好可以解决上面的问题, 因为只需要维护数组的前kkk个小值,空间是固定的。 当数组超级大, 全存入内存都变得不可行的时候, 就需要从外存中慢慢读取数组,然后和这个堆进行排序。【求最大 top k 问题用最小堆,求最小 top k 问题用最大堆】, 这是因为我们先建立一个kkk的堆,然后后面的数进行插入,如果一旦建立小顶堆,那么堆顶是最小元素,这时候,一旦遍历后面的数进行调整的时候, 发现有更小的,就只能替换掉堆顶元素,没法往下调整了, 下面的节点就会保持不变,这个情况下,根本无法保证下面的节点是前K个最小里面的。所以,只有建立大顶堆之后,才能用后面小的数替换大顶堆中的任何节点。

    1. 首先,建立一个长度为kkk的大顶堆
    2. 随后从k+1k+1k+1个数开始遍历,如果当前遍历到的数比大顶堆的堆顶数要小, 就把堆顶的数弹出,再插入当前遍历到的数
    3. 最后将大顶堆里的数存入数组返回即可

    注意: 这里如果使用python的库,自带的是小顶堆, 因此,需要对数组中所有数去相反数,才能使用小顶堆维护前k个值。这里先给出一个手撸版大顶推的代码,然后再给出一个调用python库实现的一个代码。


    下面看下如果调包的话,应该怎么使用, 这次也正好用用python自带的堆排序。注意python里面维护的是小根堆,如果想用大根堆,取反。笔试的时候推荐使用下面的方式:

    或者笔试的时候,直接heapq.nlargest(k, arr)或者heapq.nsmallest(k,arr)得到前K个最大或者最小值。牛客上的一道高频题目,是不仅要找到前K个最小的,并且还得按照从小到大再排序进行输出, 那这样的话,自己写的那个,还需要再写一个堆排序,返回结果。

    class Solution:def GetLeastNumbers_Solution(self, tinput, k):if len(tinput) == 0 or k > len(tinput): return []if k == 0:return []# write code heredef heapify(tree, n, i):c1 = 2*i + 1c2 = 2*i + 2max_ele = iif c1 < n and tree[c1] > tree[max_ele]: max_ele = c1if c2 < n and tree[c2] > tree[max_ele]: max_ele = c2if max_ele != i:tree[max_ele], tree[i] = tree[i], tree[max_ele]heapify(tree, n, max_ele)def buildheap(tree, k):# 建立一个大小为k的堆last_node = k - 1parent = (last_node-1) // 2for i in range(parent, -1, -1):heapify(tree, k, i)def get_smallest(tinput, n, k):# 拷贝前k个元素tree = tinput[:k]# 建堆buildheap(tree, k)# 从k+1开始for i in range(k, n):if tinput[i] < tree[0]:tree[0] = tinput[i]heapify(tree, k, 0)return treedef heapSort(tinput, n, k):tree = get_smallest(tinput, n, k)for i in range(k-1, -1, -1):tree[0], tree[i] = tree[i], tree[0]heapify(tree,i, 0)return treereturn heapSort(tinput, len(tinput), k)
    
  • LeetCode215: 数组中的第K个最大元素: 这个题和上面这个基本上一模一样, 只不过这里是找第K大,上面是找前K小, 一样的模板和代码, 这里是从大到小排序,如果发现中轴位置等于k了, 返回k-1位置的数即可。


    所以,找第K大,前K大,第K小,前K小的题目,都可以这么玩了。当然,堆排序会更加简单,而且更快

  • LeetCode347: 前K个高频元素: 遍历一遍数组,统计每个字符的个数。 根据字符的个数维护一个小顶堆,相当于根据字符个数排序,然后返回前K个即可。和上面差不多。这里可以可以写一个小根堆,也可以掉包, 由于是刚练习,所以都写一下,笔试的时候建议直接掉包。

    下面给个掉包版了,这个就非常舒服了。

  • 年龄排序: 这个题目来自于剑指offer, 对公司员工的年龄排序,但要求时间复杂度是O(n)O(n)O(n),这里会允许使用O(n)O(n)O(n)的辅助空间。这个题目的思路我觉得是非常不错的,所以就摘过来了。 由于需要排序的数字是员工年龄,范围有限。所以我们可以先采用一个100的辅助空间来统计各个年龄出现的次数,然后从1遍历到99, 看看每个年龄出现了几次,直接排序即可。 代码大体是这样:

    def SortAges(ages, lenght):# 定义最大年龄是99oldestAge = 99timesOfAge = defaultdict(int) # 统计每个年龄出现的次数# 遍历ages里面的每个员工, 统计出现次数for i in range(length):timesOfAge[ages[i]] += 1# 下面开始排序即可index = 0for i in range(oldestAge+1):for j in range(timesOfAge[i]):ages[index] = iindex += 1
    

    排序这块一定要打破一种思维定势, 总感觉时间复杂度O(n)O(n)O(n)是一个不太可能的问题,但是有了空间辅助,这个就不一定不可能了。这个题的记录也是提醒下自己,因为一见到时间复杂度O(n)O(n)O(n)我自己就有点害怕,包括一次面试里面的1-n数字排序,也就是原地哈希的思路,这个其实也是能达到的,具体的可以看数组那块的整理。 但一旦有了一种害怕心理,那显然是达不到了。所以遇到问题先不要慌,先冷静思考一番,不要被一些定势给困住

  • LeetCode295: 数据流中的中位数: 这个题还是非常考察数据结构理解功底的, 数据流中找中位数, 数据流是可以任意时刻添加数字的,所以在这种动态的地方找中位数,对于数据结构的选取还是非常有讲究的,在剑指offer上(P215), 作者分析了用数组,链表,二叉搜索树,AVL树以及最大堆和最小堆组合的插入和得到中位数的时间复杂度, 综合考虑,这个题最好的方式就是最大堆和最小堆结合的方式。 我们知道,一个数组里面的中位数位于中间位置,如果此时中位数左边的部分比右边的部分小(这两边内部的大小不管), 那么这时候, 获取中位数就非常简单,偶数的时候,就是左边部分最大值与右边部分的最小值之和的1/2, 奇数的时候,就是右边部分的最小值。而最大堆和最小堆正好可以在O(1)O(1)O(1)的时间内获取到最大值和最小值。 所以这个思路的具体步骤是这样:

    1. 插入操作的时候
      如果是当前数据流中个数是偶数,那么就把新数插入到右边最小堆,但这样可能不能保证左边的元素恒小于右边元素,所以还得先判断下,如果发现左边的最大值大于当前插入元素了,这时候,要把左边的最大值插入到右边, 而新数插入到左边去; 同理, 如果个数是奇数, 本应该插入到左边最大堆, 但也得判断下,这个数是不是大于右边的最小值了,如果是,需要把右边最小值插入到左边,当前数插入到右边
    2. 访问中位数
      这个如果当前是个数是偶数, 那就是最大堆堆顶元素与最小堆堆顶元素和的1/2,否则就是最小堆堆顶元素

    具体代码实现的时候,这里直接用了heapq库进行的调整,但注意这里面只能维护最小堆, 如果想建立最大堆,需要把数据进行取反


    这个题的思路还是非常好的。又锻炼了一波python的heapq操作, 一定要切记最大堆的建立这个,尤其是上面插入的时候,一不小心就会出错。

  • 剑指offer45: 把数组排成最小的数: 这个题需要自定义排序规则, 传入两个字符串x,y,比较的是两个字符串拼接完之后的数的结果:

    1. if xy > yx: 此时说明我们应该把y放到x的前面,即y < x
    2. if xy < yx: 此时说明我们应该把x放到y的前面,即x < y

    所以可以使用快速排序, 但是这里比大小的时候规则要进行改变,至于这么做的原因,可以看后面题解里面大佬的分析了。


    这里还可以用python内置的快排函数, 主要是学习下如何自定义规则:

    python里面排序函数排序的原理,其实就是根据内建的cmp函数排序

    对于cmp(x,y):
    如果想要x排在y前面那么返回一个负数,如果想x排在y后面那么返回一个正数
    

    而我们要想自定义排序规则,就需要自己定义个这样的函数,写下规则,然后传给排序函数,在python2中,有一个cmp参数,直接接受自定义函数的,而python3里面,需要借助functools里面的cmp_to_key函数来完成。具体的可以参考这篇文章

  • LeetCode179: 最大数: 上面这个题的变形,这次是拼成最大数了, 和上面一样的道理, 只不过相当于基于规则从大到小排序。 这里还要注意一点,那就是前导0, 需要去除

  • 剑指offer51: 数组中的逆序对: 这个题可以用归并排序来解, 逆序对的统计就是在归并排序两个有序数组合并的时候, left[m-1] > right[n-1]的里面进行统计,因为当这个条件成立的时候,说明此时有n个逆序对。 这样统计完之后,排序,最后就能得到结果。

3. 小总

排序算法里面比较重要且常考的是归并,快速和堆排序, 而比较重要的题目就是找第K大或者小,前K大或者小,最后总结如下:

  • 面试题17.14: 最小的K个数
  • LeetCode215: 数组中的第K个最大元素  ⭐️⭐️⭐️⭐️⭐️⭐️
  • LeetCode347: 前K个高频元素 ⭐️
  • 年龄排序
  • LeetCode295: 数据流中的中位数
  • 剑指offer45: 把数组排成最小的数
  • LeetCode179: 最大数
  • 剑指offer51: 数组中的逆序对

参考

  • 用Python手写十大经典排序算法 — 各种动画演示
  • 【常考排序算法】堆排序

算法刷题重温(九): 排序算法来啦相关推荐

  1. 力扣(LeetCode)怎么刷题,以排序算法为例

    掌握 LeetCode 刷题方法再开始刷题,属于磨刀不误砍柴工.掌握正确方法是非常重要的. 如果你在刷题的时候发现怎么也写不出来,别担心,这是正常的.如果你还发现,之前明明刷过的题,过段时间再做的时候 ...

  2. 算法刷题重温(八): 硬核动态规划

    1. 写在前面 这篇文章复习动态规划系列的题目, 这里由于我是刚走完了一遍之后过来的总结复刷, 所以应该印象会更加深刻一点,所以赶紧把学到的知识点和题目整理思路总结过来,便于后面的再复习.这次主要是跟 ...

  3. 算法刷题7(C++)BFS算法

    一.算法概述 BFS算法本质就是从起点到终点找到最近的距离. BFS算法核心把一个问题抽象成图,从一个点开始,向四周开始扩散. 比DFS的空间复杂度大. 二.算法框架 int BFS(Node sta ...

  4. 搬砖试金石!github星标7W算法刷题宝典,还愁拿不下大厂offer?

    前言 这几年IT技术蓬勃发展,日新月异,对技术人才的需求日益增长,程序员招聘市场也如火如荼.在有限的三五轮面试中,国外流行让面试者编程解决某些数据结构和算法的题目,通过观察面试者编码的熟练程度.思考的 ...

  5. 一夜登顶GitHub!字节内网数据结构与算法刷题笔记,看完直呼卧槽

    网络上流传着一句段子"程序员两条腿,一条是算法,一条是英文,想跑的更远,这两条腿都不能弱".英文,我们暂且不谈,我们先来谈谈算法. 算法之难,在于将精巧的逻辑,通过合适的数据结构, ...

  6. 神了,无意中发现一位1500道的2021LeetCode算法刷题pdf笔记

    昨晚逛GitHub,无意中看到一位大佬的算法刷题笔记,感觉发现了宝藏!有些小伙伴可能已经发现了,但咱这里还是忍不住安利一波,怕有些小伙伴没有看到. 关于算法刷题的困惑和疑问也经常听朋友们提及.这份笔记 ...

  7. 找到所有数组中消失的数字_【一点资讯】千万程序员的呼声:面试如何拿到大厂Offer?这份阅读量超过11W+的算法刷题宝典请你原地查收 www.yidianzixun.com...

    如何才能通过面试拿到大厂Offer? "刷leetcode!" 这是我听到最多的回答! 现在越来越多的人应聘工作时都得先刷个几十百来道题,不刷题感觉都过不了面试. 无论是面测试.算 ...

  8. 深度优先搜索dfs算法刷题笔记【蓝桥杯】

    其实网上已经有不少dfs的算法笔记,但我之所以还再写一篇,主要是因为我目前见到的笔记,都有些太偏向理论了. 对于基础薄弱的或是没有基础的人(like me),有点不合适,因为看了,也不能说自己会了. ...

  9. ACM-ICPC 常用算法刷题网站整理

    ACM-ICPC 常用算法刷题网站整理 转载From http://blog.csdn.net/bat67/article/details/72765485 以及http://blog.csdn.ne ...

最新文章

  1. 3.2.4 控制图层显示的范围
  2. ntrip获取源列表_Ntrip通讯协议怎么样?
  3. 虚拟机扩容后mysql无法使用_VMWARE 扩容踩坑记
  4. 关于python中excel写入案例
  5. 猜数字游戏的提示(UVa340)
  6. 前端开始学java_[Java教程]开启前端学习之路
  7. 集合添加元素python_Python 集合(Set)
  8. php开发电商项目的技术,[项目实战] php电商开发基本功课程 电商后台实战开发视频教程 共6章...
  9. 中国OpenJDK联盟发行特别版本的想法
  10. 如何用VS2015手撕白菜
  11. ClustalX进行多序列比对流程
  12. 通过命令行安装egret引擎
  13. 形容java工作者的句子_关于形容工作态度的句子
  14. Datawhale组队学习周报(第026周)
  15. 互联网应用 zzl复习版
  16. 互联网人工智能下的OCR文字识别
  17. Systemverilog实现参数化的Round-Robin Arbiter Tree
  18. 百度小程序和微信小程序,开放与封闭之争
  19. 全志 H6 Orange Pi Lite 2 Android 7.0 UART配置
  20. java派生类属例子_“派生”类属与“相关”类属的辨析

热门文章

  1. 山东大学博士 计算机 统考,报考山大统招博士需谨慎
  2. 第7章 嵌入式uClinux及其应用开发(1)
  3. 敏捷开发篇--Agile Development-自用
  4. 莫队入门例题之持久化莫队:2120: 数颜色
  5. 这个立冬,我线下面基了一位TMD高级专家,太牛逼了!
  6. HTML复选框,点击文字,对应的复选框选中
  7. 一个奇鸽软件应用有病毒吗?
  8. 最小二乘法,简明公式整理,数学证明,matlab程序(自写代码、lsqcurvefit函数、fminsearch函数)
  9. Unity提供的消息推送机制
  10. 企业不得不知的BYOD实施十大风险