二分查找、快排、堆排序

查找(二分查找),经典排序(冒泡排序、选择排序/插入排序、谢尔排序、归并排序、快速排序)
常考问题:topk 即找出前k大或者前k小的数,使用快排和堆排序实现。补充二叉堆的概念理解和功能实现。

二分查找

二分查找的递归算法

——返回T F

### 二分查找的递归算法
###  如果只剩下1个数字,直接对比,如果不是则不存在
def binarySearch(alist,item):
    # 停止条件
    if not alist:
        return False
    if len(alist) == 1:
        return alist[0]==item
    # 递归
    mid = len(alist)//2
    return binarySearch(alist[:mid], item) or binarySearch(alist[mid:], item)

print(binarySearch([1,3,5,8,12,19,25], 8))
True

二分查找的双指针算法(重要)

稍微改造一下可以返回下标值,并且复杂度更优

def binarysearch(nums, target):
    if not nums:
        return False
    l, r = 0, len(nums) - 1
   
    while l <= r:
        if target < nums[l] or target > nums[r]:
            return False
        mid = (l + r) // 2
        if target == nums[mid]:
            return True
        if target > nums[mid]:
            l = mid + 1
        if target < nums[mid]:
            j = mid -1
    return False      

print(binarySearch([1,3,5,8,12,19,25], 2))
False

排序

冒泡排序

  • 对无序表多趟比较,每趟多次相邻对比交换,并交换。第i躺排序第i大的项。
  • 稍加改进的版本:加入exchange, 如果这一趟没有exchange 说明排序完成了, 直接break
### 1.原始版本
nums = [34,82,66,14,56,80,9,44,69,21,60,65]
for i in range(len(nums)):
    for idx in range(len(nums)-1):
        if nums[idx] > nums[idx+1]:
             nums[idx], nums[idx+1] = nums[idx + 1], nums[idx]
    print(nums)

print('\n改进版本:\n')
### 2.加入exchange的改进版本
nums = [34,82,66,14,56,80,9,44,69,21,60,65]
for i in range(len(nums)):
    exchange = False
    for idx in range(len(nums)-1):
        if nums[idx] > nums[idx + 1]:
            exchange = True
            nums[idx], nums[idx + 1] = nums[idx + 1], nums[idx]
    if not exchange: break
    print(nums)
[34, 66, 14, 56, 80, 9, 44, 69, 21, 60, 65, 82]
[34, 14, 56, 66, 9, 44, 69, 21, 60, 65, 80, 82]
[14, 34, 56, 9, 44, 66, 21, 60, 65, 69, 80, 82]
[14, 34, 9, 44, 56, 21, 60, 65, 66, 69, 80, 82]
[14, 9, 34, 44, 21, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 34, 21, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]

改进版本:

[34, 66, 14, 56, 80, 9, 44, 69, 21, 60, 65, 82]
[34, 14, 56, 66, 9, 44, 69, 21, 60, 65, 80, 82]
[14, 34, 56, 9, 44, 66, 21, 60, 65, 69, 80, 82]
[14, 34, 9, 44, 56, 21, 60, 65, 66, 69, 80, 82]
[14, 9, 34, 44, 21, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 34, 21, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]

选择排序/插入排序

和动态规划的感觉类似,每一次迭代完成前i项的排序

前k项已经从小到大排序了,现在看第k+1个

  • 顺序对比第k+1个应该插入到前K项的哪里,即对于前k项从右向左遍历,如果大于k+1项则往后挪1
  • 直到前K项中,所有大于第K+1项的都已经依次往后挪了一位,空出来的那位插入第K+1项
  • 结果——前k+1项已经从小到大排序了
nums = [34,82,66,14,56,80,9,44,69,21,60,65]
for idx in range(1, len(nums)):
    tmp = nums[idx]
    j = 1
    while j <= idx and nums[idx - j] > tmp:  #  前面所有小于tmp 的都依次往后挪1
        nums[idx - j + 1] = nums[idx - j] 
        j += 1
    nums[idx - j + 1] = tmp   # 多出来的位置插入tmp
    print(nums)
[34, 82, 66, 14, 56, 80, 9, 44, 69, 21, 60, 65]
[34, 66, 82, 14, 56, 80, 9, 44, 69, 21, 60, 65]
[14, 34, 66, 82, 56, 80, 9, 44, 69, 21, 60, 65]
[14, 34, 56, 66, 82, 80, 9, 44, 69, 21, 60, 65]
[14, 34, 56, 66, 80, 82, 9, 44, 69, 21, 60, 65]
[9, 14, 34, 56, 66, 80, 82, 44, 69, 21, 60, 65]
[9, 14, 34, 44, 56, 66, 80, 82, 69, 21, 60, 65]
[9, 14, 34, 44, 56, 66, 69, 80, 82, 21, 60, 65]
[9, 14, 21, 34, 44, 56, 66, 69, 80, 82, 60, 65]
[9, 14, 21, 34, 44, 56, 60, 66, 69, 80, 82, 65]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]

谢尔排序

  • 按照间隔划分子列表,分别插入排序后,整体更加接近有序。
  • 间隔越来越小,最后间隔为1时进行的插入排序,复杂度和对无序进行插入排序完全不同。
  • 复杂度为o(n^1.5)
nums = [34,82,66,14,56,80,9,44,69,21,60,65]
def gapsort(nums, gap):
    for i in range(gap, len(nums), gap):
        # 进行排序
        tmp =  nums[i]
        j = gap
        while j <= i and nums[i - j] > tmp:
            nums[i - j + gap] = nums[i - j]
            j += gap
        nums[i - j + gap] = tmp
    
    print([nums[i] for i in range(0, len(nums), gap)])  # 输出子列的排序结果
    return nums

def shellsort(nums):
    gap = len(nums)//2
    while gap>0:
        nums = gapsort(nums, gap)
        print(nums)     # 一次gap排序的结果
        gap = gap // 2
    return nums

shellsort(nums)
[9, 34]
[9, 82, 66, 14, 56, 80, 34, 44, 69, 21, 60, 65]
[9, 14, 21, 34]
[9, 82, 66, 14, 56, 80, 21, 44, 69, 34, 60, 65]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]





[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]

归并排序

def mergesort(nums):
    if len(nums) <= 1:
        return nums
    mid = len(nums) // 2
    left = mergesort(nums[:mid])
    right = mergesort(nums[mid:])

    # merge
    merged = []
    while left and right:
        if left[0] <= right[0]:
            merged.append(left.pop(0))
        else:
            merged.append(right.pop(0))
    merged.extend(left if left else right)
    return merged
    
nums = [34,82,66,14,56,80,9,44,69,21,60,65]
mergesort(nums)
[9, 14, 21, 34, 44, 56, 60, 65, 66, 69, 80, 82]

快速排序

思想:首先找一个基准值,设置一个左标和右标,从两侧开始:

  • 左标对应数字应该小于基准值,如果大于,左标暂时不动,等待与右标交换
  • 右标对应数字应该大于基准值,如果小于,右标暂时不动,等待与左标交换
  • 左标往右移动,右标往左移动

使得list前面的都小于基准值,后面的都大于基准值

对前面的部分和后面的部分分别递归——最后+连接起来,递归终值条件为长度<=1

def fastsort(nums):
    if len(nums) <= 1:
        return nums
    
    baseline = nums[0]
    left, right = 1, len(nums) - 1
    while left < right:
        if nums[left] < baseline:
            left += 1
        elif nums[right] > baseline:
            right -= 1
        elif nums[left] > baseline > nums[right]:
            nums[left], nums[right] = nums[right], nums[left]
    if nums[left] < nums[0]:
        nums[left], nums[0] = nums[0], nums[left]
    print(nums)
    l = fastsort(nums[:left])
    r = fastsort(nums[left:])

    return l + r

nums = [32,34,82,66,14,56,80,9,44,69,21,60,65]
fastsort(nums)
[32, 21, 9, 14, 66, 56, 80, 82, 44, 69, 34, 60, 65]
[14, 21, 9, 32]
[14, 9, 21]
[9, 14]
[66, 56, 65, 60, 44, 34, 69, 82, 80]
[34, 56, 65, 60, 44, 66]
[34, 56, 65, 60, 44]
[56, 44, 60, 65]
[44, 56]
[60, 65]
[69, 82, 80]
[80, 82]





[9, 14, 21, 32, 34, 44, 56, 60, 65, 66, 69, 80, 82]

topk 类问题

  • 简单的可以用排序稍微变换一下,比如用快排
  • 用堆排序

快排

e.g. 剑指 Offer 40. 最小的k个数

# topk 快排问题
nums = [32,34,82,66,14,56,80,9,44,69,21,60,65]

def minK(nums, k):
    if k < 1:
        return []
    if k >= len(nums):
        return nums

    tmp = nums[0]
    left, right = 1, len(nums) - 1
    while left < right:
        if nums[right] < tmp < nums[left]:
            nums[right], nums[left]  = nums[left], nums[right]
        elif nums[left] <= tmp:
            left += 1
        elif nums[right] >= tmp:
            right -= 1

    if nums[left] < tmp:
        nums[0], nums[left] = nums[left], nums[0]
    
    # 递归
    if left >= k:
        return minK(nums[:left], k)
    if left < k:
        return nums[:left] + minK(nums[left:], k - left)


# 测试
minK([32,21,82,66,14,56,80,9,44,69,21,60,66], k = 5)
[32, 21, 21, 9, 14]

堆排序

我们可以使用一个大小为 k 的最大堆(大顶堆),将数组中的元素依次入堆,当堆的大小超过 k 时,便将多出的元素从堆顶弹出

什么是堆——https://www.bilibili.com/video/BV1K4411X7fq?from=search&seid=15213146471374609847

class Solution:
    def getLeastNumbers(self, arr, k):
        if k == 0:
            return list()

        hp = [-x for x in arr[:k]]    # python 里面是最小堆,也就是根节点是最小值,所以用负号逆转过来
        heapq.heapify(hp)             # 前k项先直接加入堆
        
        for i in range(k, len(arr)):
            if -hp[0] > arr[i]:          # 如果当前值 > 堆里最大值
                heapq.heappop(hp)  # 就把最大值pop出来,把当前值加进去
                heapq.heappush(hp, -arr[i])
        ans = [-x for x in hp]
        return ans

二叉堆

二叉堆的概念理解

理论部分:

用来做什么的?实现优先队列

用二叉堆来实现“优先队列”。优先队列和队列一样队尾进队首出,但是优先队列内有次序,优先级高的要作为VIP插队到前排。
当然我们可以简单的用插入+排序来实现优先队列,但是插入复杂度o(n)排序复杂度o(nlogn)。如果用二叉堆实现,出入队列复杂度都是o(logn)!

二叉堆的结构?完全二叉树

二叉堆是一颗完全二叉树,即节点都有左右两个子节点,最多可有一个节点例外。这个性质使得树可以用数组(比self.left&right这种链式存储更简单高效)来存储:

当第一个元素放在索引为0的位置上时:

  1. 索引为 i 的左孩子的索引为(2*i + 1)

  2. 索引为 i 的右孩子的索引为 (2*i + 2)

  3. 索引为 i 的父节点的索引为 (i - 1)// 2

堆次序?最大堆和最小堆

分为两种:最大堆和最小堆。

  • 最大堆:父结点的键值总是大于或等于任何一个子节点的键值;
  • 最小堆:父结点的键值总是小于或等于任何一个子节点的键值

实现二叉堆的功能

这里以最大堆为例

1. insert

先放到列表尾,如果新加入的数据项比父节点要大,把它与父节点互换位置——不断上浮直到正确位置。

class MaxHeap:
    def __init__(self):
        self.heapList = []
        
    def insert(self, new):
        self.heapList.append(new)  # 1.插入到队尾
        self.percUp()     # 2.上浮操作
        
    def percUp(self):
        cur = len(self.heapList) - 1 
        while cur >= 1:
            parent = (cur - 1) //2
            if self.heapList[parent] < self.heapList[cur]:
                self.heapList[parent], self.heapList[cur]  = self.heapList[cur], self.heapList[parent]
                cur = parent
            else: break
                
    def getMax(self):
        return self.heapList[0]
hp = MaxHeap()
hp.insert(1);hp.insert(4);hp.insert(3);hp.insert(11);hp.insert(7);
print(hp.heapList)
print(hp.getMax())
[11, 7, 3, 1, 4]
11

2. delMax

跟节点是最大的,但困难的是移走根节点后如何恢复堆结构和堆次序——分两步走:

  • 用最后一个节点来代替根节点。
  • 将新节点“下沉”来恢复堆次序,同样通过交换——如果小于子节点,和左右子节点中较大的进行交换
class MaxHeap(MaxHeap):
    
    def maxChild(self, i):   # 返回最大child 的索引
        left, right = 2 * i + 1,  2 * i + 2
        length = len(self.heapList) - 1 
        
        if length >= right:  # 左右都存在
            return left if self.heapList[left] > self.heapList[right]  else right  # return maxchild
        if length < left:      # 没有子节点
            return None
        else:                     # 只有左节点
            return left
    
    def percDown(self):     # 下沉
        cur = 0
        while self.maxChild(cur):
            maxchild = self.maxChild(cur)
            if self.heapList[cur] < self.heapList[maxchild]:
                self.heapList[cur], self.heapList[maxchild] = self.heapList[maxchild], self.heapList[cur]
            cur = maxchild
            
    def delMax(self):
        tmp = self.heapList[0]
        if len(self.heapList) == 1:
            self.heapList.pop()
        if len(self.heapList) > 1:
            self.heapList[0]  = self.heapList.pop()
            self.percDown()
        return tmp

利用del操作可以实现排序:

hp = MaxHeap()
hp.insert(1);hp.insert(4);hp.insert(7);hp.insert(11);hp.insert(3);

print(hp.heapList)
# 测试删除操作
res_sorted = []
while hp.heapList:
    res_sorted.append(hp.delMax())
    print(hp.heapList)

res_sorted   # 每次删掉的都是最大值——组成从大到小的顺序list,完成了堆排序
[11, 7, 4, 1, 3]
[7, 3, 4, 1]
[4, 3, 1]
[3, 1]
[1]
[]





[11, 7, 4, 3, 1]

3.用自己的函数进行堆排序——min k 问题

输入:数字list + k

输出:最小的k个数字——用大根堆

def getLeastNumbers(arr, k):
    if k >= len(arr):
        return arr
    
    hp = MaxHeap()
    # 1.先加入k个
    for i in range(k):
        hp.insert(arr[i])

    # 2.只保留当前最小的k个
    for j in range(k, len(arr)):
        if hp.getMax() > arr[j]:
            hp.delMax()
            hp.insert(arr[j])
    
    return hp.heapList

arr = [32,21,82,66,14,56,80,9,44,69,21,60,66]
getLeastNumbers(arr, k = 4)
[21, 21, 14, 9]