算法介绍(二分,冒泡,选择,插入;快排,堆排,归并)

1.概念

  算法:一个计算过程(函数),或者说是解决问题的方法可以理解成一个算法

  时间复杂度:用来估算算法运行时间的一个式子(单位)。一般来说,时间复杂度高的算法比复杂度低的算法慢

  空间复杂度:用来估算算法占用内存的一个式子

1.1 常见时间复杂度按照效率排序

  O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)

  一般来说

      循环减半的过程,时间复杂度通常是O(logn)

      几次循环,时间复杂度就是n的几次方

1.2 案例

"""
一段有n个台阶组成的楼梯,小明从楼梯的最底层向最高处前进,它可以选择一次迈一级台阶或者一次迈两级台阶。问:他有多少种不同的走法?
"""

思路:

  假设上 n 级台阶有 f(n)种方法,把这 n 种方法分为两大类,第一种最后一次上了一级台阶,这类方法共有 f(n-1)种,第二种最后一次上了两级台阶,这种方法共有 f(n-2)种,则得出递推公式f(n)=f(n-1)+f(n-2),显然,f(1)=1,f(2)=2,递推公式如下:

def steps(n):
    if n <= 2:
        return n
    else:
        return steps(n-1) + steps(n-2)

print(steps(10))

这种方式简单,但是效率低,很有可能超出递归最大层数

我们可以考虑采用循环来做

1.3 二分排序法(时间复杂度O(logn))

  这里针对的是一个有序列表,一般是在算法中输入该列表中某个数,然后输出这个数在列表中的位置,即索引

示意图:

代码:

def bin_search(key, lis):
    low = 0     #
    high = len(lis) - 1     #
    while low < high:
        mid = (low + high) // 2
        if lis[mid] == key:    # 命中
            return mid
        elif lis[mid] > key:  # key在左边,说明key的范围在low到mid-1之间
            high = mid - 1      
        elif lis[mid] < key:  # key在右边,说明key的范围在high+1到high之间
            low = mid + 1

利用递归实现二分查找(可以使用bisect模块来实现):

def bin_search_2(key, lis, low, high):
    if low <= high:
        mid = (low + high) // 2
        if lis[mid] == key:
            return mid
        elif lis[mid] > key:    # 在左边,说明key在low和mid-1之间
            return bin_search_2(key,lis,low, mid-1)
        else:                   # 在右边,说明key在mid+1和high之间
            return bin_search_2(key,lis,mid+1,high)
    else:
        return

 对于一个二维列表实现的二分查找:

li = [
    [1, 3, 5, 7],
    [9,10,14,17],
    [18,22,25,30]
]

代码:

def binary_search_2d(li, val):
    left = 0
    m = len(li)
    n = len(li[0])
    right = m * n - 1
    while left <= right:    # 候选区非空
        mid = (left + right) // 2
        i = mid // n       # 取整
        j = mid % n        # 取余
        if li[i][j] == val:
            return i, j
        elif li[i][j] < val:
            left = mid + 1  # 更新候选区
        else: # >
            right = mid - 1 # 更新候选区
    return None

print(binary_search_2d(li, 22))

2. 排序入门篇

2.1 冒泡排序(时间复杂度O(n**2))

  针对的是列表中的两个相邻的数,如果前边比后面的大,则交换这两个数,如此持续进行

  代码关键点在于几趟以及无序区

示例代码1:

def bubble_sort(lis):
    # j表示每次便利需要遍历的最小次数,它是逐渐减小的
    for j in range(len(lis)-1,0,-1):
        for i in range(j):
            if lis[i] > lis[i+1]:
                lis[i],lis[i+1] = lis[i+1], lis[i]

lis = [9,5,2,7,6,8,3,1,4]
bubble_sort(lis)
print(lis)

示例代码2:

import random

def bubble_sort(li):
    for i in range(len(li)-1): # i表示第i趟
        for j in range(len(li)-i-1): # j表示箭头的下标
            if li[j] > li[j+1]:  # 后面的数比前面的数大
                li[j], li[j+1] = li[j+1], li[j]
        print(li)
            
                
li = list(range(10))
random.shuffle(li)
bubble_sort(li)

 以上代码忽视了极端情况,从运行效率上来讲,我们还可以在做优化,排除已经是有序的列表

import random

def bubble_sort_2(li):
    for i in range(len(li)-1): # i表示第i趟
        exchange = False
        for j in range(len(li)-i-1): # j表示箭头的下标
            if li[j] > li[j+1]:     # 如果前面比后面大
                li[j], li[j+1] = li[j+1], li[j]
                exchange = True
        if not exchange:
            return
        print(li)

li = list(range(10))
random.shuffle(li)
bubble_sort_2(li)

2.2 选择排序(时间复杂度O(n**2))

选择排序第一种代码:

  一趟遍历记录最小的数,放到第一个位置;

  再一趟遍历记录剩余列表中最小的数,继续放置;

  关键点在于无序区以及最小数的位置

代码:

import random

def select_sort(li):
    for i in range(len(li)-1):  # i可以理解成趟数,这里看做索引也行
        min_pos = i # 最小值默认为无序区第一个数 i
        for j in range(i+1, len(li)):
            if li[j] < li[min_pos]:
                min_pos = j
        li[i], li[min_pos] = li[min_pos], li[i]


li = list(range(100))
random.shuffle(li)
select_sort(li)
print(li)

选择排序第二种代码:

  这里我也可以先去查找数组中最小值的索引,然后再追加的方式来排序(好理解,但是代码量变大)

# 查找最小值的索引
def findSmanllest(arr):
    smallest = arr[0]
    smallest_index = 0
    for i in range(1,len(arr)):      # 这里应该是range(1,len(arr))还是range(len(arr))??尝试两个都可以
        if arr[i] < smallest:
            smallest = arr[i]
            smallest_index = i
    return smallest_index       # 返回数组中最小值的索引

# 进行排序
def selectionSort(arr):
    newarr = []
    for i in range(len(arr)):
        smallest = findSmanllest(arr)
        newarr.append(arr.pop(smallest))    # 追加数组中指定索引的值
    return newarr

print(selectionSort([5,3,6,2,10,41,1]))

2.3 插入排序(时间复杂度O(n**2))

  列表被分为有序区和无序区两个部分。最初有序区只有一个元素。

  每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。(类似我们玩纸牌)

  代码关键点在于我们摸到的牌,以及手里的牌

 代码:

import random

def insert_sort(li):
    for i in range(1, len(li)): # i表示摸到牌的位置,也表示趟
        j = i - 1       # j是当前用来比较的牌
        tmp = li[i]     # 摸到的牌
        while j >= 0 and li[j] > tmp:   # 摸到的牌和他前面位置(前面位置必须大于等于0)的牌作比较
            li[j+1] = li[j]     # 前面的牌比手里的大,把它往后移
            j -= 1      # 再去看前面那张牌
        li[j+1] = tmp   # 用来比较的牌比摸到的牌小,直接放到j+1的位置


li = list(range(100, -1, -1))
random.shuffle(li)
insert_sort(li)
print(li)

3. 排序升级篇

3.1 快排(时间复杂度O(n*log(n)))      --->算法里面简单,也是必须要掌握的一个

  取一个元素p(第一个元素),使元素p归位;

  列表被p分成两部分,左边都比p小,右边都比p大;

  递归完成排序。

  该算法的关键点是先对数组进行整理,然后再做递归操作

代码第一部分

def quick_sort(lis, left, right):
    if left < right:
        mid = partition(lis, left, right)  # 返回中间这个值
        quick_sort(lis, left, mid - 1)      # 对左边的进行排序
        quick_sort(lis, mid + 1, right)     # 对右边的进行排序

但是怎么对它做p归位处理呢?这里我们先取出第一个数,即5,5的空位需要从右边往左找一个比5小的数填充,即2

2放在了5的空位上,现在需要再从左边取一个比5大的数放在2的空位上,即7

7放在了2的空位上,现在需要再从右边取一个比5小的数放在7的空位上,即1

---

代码第二部分(做归位处理)

def partition(lis, left, right):
    tmp = lis[left]  # 先取出最左边这个数,存起来
    while left < right:  # 从右边找比tmp小的数
        while left < right and lis[right] >= tmp:
            right -= 1  # 如果右边的数总比取出的数大,则往左前进一位继续找直到找见为止
        lis[left] = lis[right]  # 找见比tmp小的数,此时插入到左边空出的那个位置
        while left < right and lis[left] <= tmp:  # 从左边找比tmp大的数
            left += 1  # 如果左边的数总比取出的数小,则往右前进一位继续找直到找见为止
        lis[right] = lis[left]  # 找见比tmp大的数,此时插入到右边空出的那个位置
    lis[left] = tmp  # 再把中间这个值tmp写回来
    return left  # 返回中间这个值,left,right最后重合在一起了


lis = [5, 7, 4, 6, 3, 1, 2, 9, 8] partition(lis, 0, len(lis) - 1) print(lis) # [2, 1, 4, 3, 5, 6, 7, 9, 8] # 可以看出5归位

以上是我们的基本思想,但是如果不从最左边取这个tmp,而是从中任意取一个数作为我们的tmp值呢?

代码:

import random


def partition(li, left, right):
    i = random.randint(left, right)  # 从中取一个随机数i
    li[left], li[i] = li[i], li[left]  # 把它调换位置放在最左边
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp:
            right -= 1
        li[left] = li[right]
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left]
    li[left] = tmp
    return left


def quick_sort(lis, left, right):
    if left < right:
        mid = partition(lis, left, right)  # 返回中间这个值
        quick_sort(lis, left, mid - 1)  # 对左边的进行排序
        quick_sort(lis, mid + 1, right)  # 对右边的进行排序


lis = list(range(10000))
random.shuffle(lis)
quick_sort(lis, 0, len(lis) - 1)
print(lis)

如果上面没有记住的话,ok可以看下面这种,这种快排方式代码更加简略和易懂,也更加pythonic

def quick_sort(lis):
    if len(lis) < 2:
        return lis
    else:
        pivot = lis[0]  # 选定的基准值,总取左边第一个值
        left = [i for i in lis[1:] if i <= pivot]  # 小于等于基准值的元素组成的列表
        right = [i for i in lis[1:] if i > pivot]  # 大于基准值的元素组成的列表
        return quick_sort(left) + [pivot] + quick_sort(right)


lis = [9, 5, 2, 8, 6, 1, 4, 7, 3]
print(quick_sort(lis))
# 把左边的数组+基准值+右边的数组
# 第一次基准值9 [5,2,8,6,1,4,7,3] + [9] + []
# 第二次基准值5 [2,1,4,3] + [5] + [8,6,7] + [9] + []
# 第三次基准值2 [1]+[2] + [4,3] + [5] + [8,6,7] + [9] + []
# 第四次基准值4 [1]+[2] + [3]+[4]+[] + [5] + [8,6,7] + [9] + []
# 第四次基准值8 [1]+[2] + [3]+[4]+[] + [5] + [6,7]+[8]+[] + [9] + []
# 第五次基准值6 [1]+[2]+[3]+[4]+[]+[5]+[]+[6]+[7] +[8] + [] + [9] + []

把上面代码一行实现

lis = [3, 6, 12, 21]

quick_sort = lambda lis: lis if len(lis) < 2 else quick_sort(
    [i for i in lis[1:] if i <= lis[0]]) + [lis[0]] + quick_sort([i for i in lis[1:] if i > lis[0]])

print(quick_sort(lis))

3.2 堆排(最复杂的一个,利用二叉树)

3.2.1 二叉树相关概念

  结点的度(Degree):一个结点拥有的子树数目称为该结点的度 

  二叉树:度不超过2的树(节点最多有两个叉)

  深度(Depth):树中结点最大层次的值

  满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。树的特点是每一层上的节点数都是最大节点数

  完全二叉树:在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树完全二叉树

      

关于二叉树的存储方式:

3.2.2 关于堆排序

  堆分为最大堆和最小堆,其实就是完全二叉树

  堆排序算法就是抓住了堆的特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。

 堆排序过程:

  1.建立堆
  2.得到堆顶元素,为最大元素
  3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
  4.堆顶元素为第二大元素
  5.重复步骤3,直到堆变空

这里我们要怎么建立堆呢???

堆排序的基本实现思路:

  给定一个列表array=[6,8,1,9,3,0,7,2,4,5],对其进行堆排序

第一步:根据该数组元素构建一个完全二叉树

第二步:初始化构造(从最后一个有子节点开始往上调整最大堆)

继续调整

继续调整

继续调整

继续调整

交换到这里的时候我们可以看到6和8其实还不满足堆的性质,所以还需要进行调整

这样就得到了一个初始堆

 我们构造好了堆,怎么一个出数呢???这里我们还要对它进行出数调整:

  每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换

  交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整

出数:

第一步:9出来,3上去

堆调整,让它满足堆的性质

第二步:8出来,3上去

堆调整,让它满足堆的性质

 

第三步:7出来,2上去

堆调整,让它满足堆的性质

第四步:6出来,1上去

堆调整,让它满足堆的性质

第五步:5出来,0上去

堆调整,让它满足堆的性质

 

第六步:4出来,0上去

堆调整,让它满足堆的性质

 第七步:3出来,1上去

堆调整,让它满足堆的性质

第八步:2出来,1上去

堆调整,让它满足堆的性质

第九步:1出来,0上去

第十步:0出来

 

说明:

  不管是初始大顶堆的从下往上调整,还是堆顶堆尾元素交换,每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换,交换之后都可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整。

 以上逻辑已经有了,下来是编写代码:

# 调整堆,单独运行只能一次调整一下
def sift(lis, low, high):
    """
    low是待调整的堆的根节点的位置索引
    high是堆的最后节点的位置,用来判断是否越界
    """
    i = low         # 当前的空位
    j = 2 * i + 1  # 左边那个孩子
    tmp = lis[i]    # 出来的那个数
    while j <= high:
        if j < high and lis[j + 1] > lis[j]:  # 右孩子存在并且右孩子大于左孩子
            j += 1  # j指向右孩子
        if tmp < lis[j]:    # 出来的数小于左边那个数
            lis[i] = lis[j] # lis[j]应该上去
            i = j       # 再重新算
            j = 2 * i + 1
        else:  # tmp比左边这个孩子大则不用动
            break
    lis[i] = tmp


# 堆排序
def heap_sort(li):
    # 先构造堆
    n = len(li)
    for i in range(n//2,-1,-1): # i是调整子树的根,从n//2开始,一直到0
        sift(li,i,n-1)      # 整个堆的最后一个节点的位置当做high
    # 挨个出数
    for i in range(n-1,-1,-1):   # i是当前堆的最后一个元素位置
        li[0],li[i] = li[i],li[0]
        sift(li,0,i-1)  # 调整


li = [6,8,1,9,3,0,7,2,4,5]
heap_sort(li)
print(li)

可以看图

第二个版本,代码比较详细,可以作为参考:

def sift_down(array, start, end):
    """
    调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
    :param array: 列表的引用
    :param start: 父结点
    :param end: 结束的下标
    :return: 无
    """
    while True:
        # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
        # 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
        left_child = 2 * start + 1  # 左孩子的结点下标
        # 当结点的右孩子存在,且大于结点的左孩子时
        if left_child > end:
            break

        if left_child + 1 <= end and array[left_child + 1] > array[left_child]:
            left_child += 1
        if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换
            temp = array[left_child]
            array[left_child] = array[start]
            array[start] = temp

            start = left_child  # 交换之后以交换子结点为根的堆可能不是大顶堆,需重新调整
        else:  # 若父结点大于左右孩子,则退出循环
            break


def heap_sort(array):  # 堆排序
    # 初始化大顶堆
    first = len(array) // 2 - 1  # 最后一个有孩子的节点(//表示取整的意思)
    for i in range(first, -1, -1):  # 从最后一个有孩子的节点开始往上调整
        sift_down(array, i, len(array) - 1)  # 初始化大顶堆

    # 交换堆顶与堆尾
    for head_end in range(len(array) - 1, 0, -1):  # start stop step
        array[head_end], array[0] = array[0], array[head_end]  # 交换堆顶与堆尾
        sift_down(array, 0, head_end - 1)  # 堆长度减一(head_end-1),再从上往下调整成大顶堆


if __name__ == "__main__":
    array = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
    heap_sort(array)
    print(array)

该代码参考来源链接:猛戳此处

以上说了这么多,能看懂已经很不错了,更何况还需要嘴啃。但是对于学Python的朋友来说,python内置模块heapq已经帮我们实现了堆排序,我们可以直接调用它实现一个堆排序

python内置模块heapq的简单使用:

import heapq

x = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]

heapq.heapify(x)
print(x)    # 一个堆排,默认小顶堆
# [0, 2, 1, 4, 3, 6, 7, 9, 8, 5]

heapq.heappush(x, -1)   # 添加一个值
print(x)    # [-1, 0, 1, 4, 2, 6, 7, 9, 8, 5, 3]

print(heapq.heappop(x)) # 取出堆顶元素
print(x)    # -1

print(heapq.nsmallest(5, [1,5,4,7,2,8,9,3,0]))  # 返回最小的5个元素
# [0, 1, 2, 3, 4]
print(heapq.nlargest(5, [1,5,4,7,2,8,9,3,0]))  # 返回最大的5个元素
# [9, 8, 7, 5, 4]

实现排序:

import heapq

def heap_sort(li):
    heapq.heapify(li)   # 先进行堆排序
    res = []
    for i in range(len(li)):
        res.append(heapq.heappop(li))
    return res

print(heap_sort([6, 8, 1, 9, 3, 0, 7, 2, 4, 5]))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

例题:

  现有n个数,设计算法找出前k大的数(k<n)

  思路:

  1.取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
  2.依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
  3.遍历列表所有元素后,倒序弹出堆顶。

# 调整堆
def sift(lis, low, high):
    # low是待调整的堆的根节点的位置,
    # high是堆的最后节点的位置,用来判断是否越界

    i = low  # 当前的空位
    j = 2 * i + 1  # 左边那个孩子
    tmp = lis[i]  # 出来的那个数
    while j <= high:
        if j < high and lis[j + 1] > lis[j]:  # 右孩子存在并且右孩子大于左孩子
            j += 1  # j指向右孩子
        if tmp < lis[j]:  # 出来的数小于左边那个数
            lis[i] = lis[j]  # lis[j]应该上去
            i = j  # 再重新算
            j = 2 * i + 1
        else:  # tmp比左边这个孩子大则不用动
            break
    lis[i] = tmp


def top(lis, k):
    heap = lis[0:k]     # 取出k之前的数
    for i in range(k // 2 - 1, -1, -1): # i是调整子树的根
        sift(heap, i, k - 1)    # 调整堆
    for i in range(k, len(lis)):
        if lis[i] > heap[0]:
            heap[0] = lis[i]
            sift(heap, 0, i - 1)
    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)


lis = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
top(lis, 3)
View Code

3.3 归并排序    时间复杂度:O(nlogn)  空间复杂度:O(n)

  将两段有序的列表合并成一个有序的列表

图解

 代码

# 两段有序排序
def merge(li, low, mid, high):
    i = low
    j = mid + 1
    li_tmp = []
    while i <= mid and j <= high:
        if li[i] <= li[j]:
            li_tmp.append(li[i])
            i += 1
        else:
            li_tmp.append(li[j])
            j += 1
    while i <= mid:
        li_tmp.append(li[i])
        i += 1
    while j <= high:
        li_tmp.append(li[j])
        j += 1
    li[low:high + 1] = li_tmp


# li = [2,5,7,8,9,1,3,4]
# merge(li,0,(len(li)-1)//2,len(li)-1)
# print(li)  # [2, 5, 7, 8, 9, 1, 3, 4]

def _merge_sort(li, low, high):
    if low < high:  # 至少有两个元素
        mid = (low + high) // 2
        _merge_sort(li, low, mid)
        _merge_sort(li, mid + 1, high)    
        merge(li, low, mid, high)
    
    
li = [2,5,7,8,9,1,3,4]
_merge_sort(li, 0, len(li)-1)
print(li)

3.5 以上算法说明

一般情况下,就运行时间而言:
  快速排序 < 归并排序 < 堆排序

三种高级排序算法的缺点:
  快速排序:极端情况下排序效率低
  归并排序:需要额外的内存开销
  堆排序:在快的排序算法中相对较慢

 3.6 算法例题

  有一个长度为 n 的数组 a,里面的元素都是整数,现有一个整数 b,写程序判断数组 a 中是否有两个元素的和等于 b?

def func(lis,d):
    count = []
    for index,i in enumerate(lis):
        for j in lis[index+1:]:
            if i +j == d:
                count.append((i,j))
    return count

lis = [2,3,8,9,16,11]
ret = func(lis,11)
print(ret)
View Code

 

参考博客:https://www.jianshu.com/p/a33aa5cf7af1

 官方演示效果参考图:猛戳此处

更多相关介绍可供参考:猛戳此处

posted @ 2018-09-24 19:09  -Learning-  阅读(1139)  评论(0编辑  收藏  举报