算法笔记_python
阅读原文时间:2023年09月02日阅读:4

目录

一个计算过程,解决问题的方法。

“程序” = 数据 + 算法

"算法" = 数据结构 + 控制流程

时间复杂度是用来估计算法运行时间的一个式子(单位)。

一般来说,时间复杂度高的算法比复杂度低的算法慢。

常见的时间复杂度(按效率排序):

​ O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n²logn) < O(n³)

复杂问题的时间复杂度:

​ O(n!) O(2^n). O(n^n)

# O(1)
print('hello world')

# O(n)
for i in range(n):
  print('hello world')

# O(n²)
for i in range(n):
  for j in range(n):
      print('hello world')

# O(n³)
for i in range(n):
  for i in range(n):
    for i in range(n):
          print('hello world')

# O(logn)
while n > 1:
  print(n)
  n = n // 2
"""
n的输出为:      ==>   2⁶ = 64
64                   log₂64 = 6
32
16
8
4           所以时间复杂度记为:O(log₂n)或O(logn)
2           ps: 当算法过程中出现循环折半的时候,复杂度式子中会出现logn
"""

上面的代码我们可以用时间复杂度来表示对应的运行效率

分别表示为:O(1) O(n) O(n²) O(n³) O(log₂n)

如何快速判断算法复杂度

确认问题规模n

循环减半过程--> logn

k层关于n的循环 --> n^k

空间复杂度:用来评估算法内存占用大小的式子

空间复杂度的表示方式与时间复杂度完全一样

​ 算法使用了几个变量:O(1)

​ 算法使用了长度为n的一维列表:O(n)

​ 算法使用了m行n列的二维列表:O(mn)

空间换时间

func3先打印在执行函数,func4先执行函数在打印

汉诺塔问题

# 使用递归解决
"""
思路:

    将汉诺塔想象为上下两部分,上面部分的就是n-1个,下面部分就剩最后一个
    经过3步最后将汉诺塔从a柱子移动到了c柱子
"""

def hanoi(n, a, b, c):
    """
    n 表示汉诺塔的层数
    a b c 的含义表示:从a经过b移动到了c(只是针对这三个对应的位置)
    """
    if n > 0:
        hanoi(n-1, a, c, b)  # 第一步:将上面部分 从a经过c移ang动到了b
        print('moving from %s to %s' % (a, c))  # 第二步:将下面部分 从a移动到了c
        hanoi(n-1, b, a, c)  # 第三步:将上面部分从b经过a移动到了c

hanoi(3, 'A', 'B', 'C')
"""
moving from A to C
moving from A to B
moving from C to B
moving from A to C
moving from B to A
moving from B to C
moving from A to C
"""

顺序查找,也叫线性查找,从列表的第一个元素开始,顺序进行搜索,直到找到元素或搜素到列表最后一个元素为止。

时间复杂度:O(n)

def liner_search(lst, val):
    for i, v in enumerate(lst):
        if v == val:
            return i
    return None

a = liner_search([1, 2, 3, 4, 5, 6, 7, 8], 3)
print(a)

二分查找:又叫折半查找,从有序列表的初始候选区li[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。

列表是有序的

时间复杂度:O(logn)

def binary_search(lst, val):
    """
    left right 指的都是lst中元素的索引, mid 指的中间值
    """
    left = 0
    right = len(lst) - 1
    while left <= right:  # 候选区有值
        mid = (left + right) // 2
        if lst[mid] == val:
            return mid
        elif lst[mid] > val:  # 待查找值在mid的左侧
            right = mid - 1
        else:  # 待查找值在mid的右侧
            left = mid + 1
    else:
        return None

a = binary_search([1, 2, 3, 4, 5, 6, 7, 8], 3)
print(a)

排序:将一组“无序”的记录序列调整为“有序”的记录序列。

列表排序:将无序列表变为有序列表

升序和降序

内置排序函数:sort()

常见的排序算法

  • 排序LowB三人组

    • 冒泡排序
    • 选择排序
    • 插入排序
  • 排序NB三人组

    • 快速排序
    • 堆排序
    • 归并排序
  • 其他排序

    • 希尔排序
    • 计数排序
    • 基数排序

冒泡排序

列表每两个相邻的数,如果前面比后面大,则交换这两个数

一趟排序完成后,则无序区减少一个数,有序区增加一个数

时间复杂度:O(n²)

import random

def bubble_sort(lst):
    for i in range(len(lst) - 1):  # i:第几趟    len(lst) - 1:  循环的趟数
        for j in range(len(lst) - i - 1):  # j: 指针  len(lst) -i - 1: 循环一趟指针经过的次数
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]

lst = [random.randint(0, 10000) for i in range(1000)]

bubble_sort(lst)
print(lst)

优化:

# 如果前面几次已经排好序了,则后面就不需要在循环
import random

def bubble_sort(lst):
    for i in range(len(lst) - 1):  # i:第几趟    len(lst) - 1:  循环的趟数
          exchange = False
        for j in range(len(lst) - i - 1):  # j: 指针  len(lst) -i - 1: 循环一趟指针经过的次数
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]
                exchange = True
        if not exchange:
          return

lst = [random.randint(0, 10000) for i in range(1000)]

bubble_sort(lst)
print(lst)

选择排序

一趟排序记录最小的数,放到第一个位置

再一趟排序记录列表无序区最小的数,放到第二个位置

算法关键点:有序区和无序区,无序区最小数的位置

时间复杂度:O(n²)

# 不推荐
def select_sort_simple(lst):
    lst_new = []
    for i in range(len(lst)):
        min_val = min(lst)
        lst_new.append(min_val)
        lst.remove(min_val)
    return lst_new
"""
时间复杂度:O(n²)
但是生成了新的列表,相当于多占用了一份内存
"""

# 推荐
def select_sort(lst):
    for i in range(len(lst) - 1):
        min_index = i  # 先认为无序区的第一个值就是最小值
        for j in range(i + 1, len(lst)):
            if lst[j] < lst[min_index]:
                min_index = j
        if min_index != i:
            lst[i], lst[min_index] = lst[min_index], lst[i]

"""
时间复杂度:O(n²)
"""

插入排序

初始时手里(有序区)只有一张牌

每次(从无序区)摸一张牌,插入到手里已有牌的正确位置

时间复杂度:O(n²)

def insert_sort(lst):
    for i in range(1, len(lst)):
        tmp = lst[i]
        j = i - 1
        """
        i: 拿取的牌的索引
        tmp: 拿取的牌
        j: 手中牌的索引
        解析:
        原列表  [5, 3, 2, 1, 4]   = >   5    [3, 2, 1, 4]
        我们可以把lst分为两部分,左边5就是我们拿在手里的牌,右边就是待拿取的牌
        """
        while j >= 0 and tmp < lst[j]:  # lst[j] 手中的牌
            lst[j + 1] = lst[j]  # lst[j] 小于新拿的牌,所以lst[j]向右移动
            j = j - 1
        lst[j + 1] = tmp
    return lst

lst = [5, 3, 2, 1, 4]
print(insert_sort(lst))

快速排序

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

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

递归完成排序

时间复杂度:O(nlogn)

# 修改递归的深度

import sys
sys.setrecursionlimit(10000)

def partition(lst, left, right):
    """
    tmp:找出的中间的那个数
    left最左边的数
    right最右边的数
    目标:找出比tem小的放到左边,比tmp大的放到右边
    """
    tmp = lst[left]
    while left < right:
        while left < right and lst[right] >= tmp:  # 从右边找比tem小的数
            right -= 1
        lst[left] = lst[right]   # 把右边的值放到左边的空位上
        while left < right and lst[left] <= tmp:
            left += 1
        lst[right] = lst[left]  # 把左边的值放到右边的空位上
    lst[left] = tmp
    return left  # 返回mid的值

def quick_sort(lst, left, right):
    if left < right:  # 至少两个元素
        mid = partition(lst, left, right)  # 第一次执行partition将列表分为左右两部分
        quick_sort(lst, left, mid - 1)
        quick_sort(lst, mid + 1, right)

lst = [5,  7, 4, 6, 3, 1, 2, 9, 8]
quick_sort(lst, 0, len(lst) - 1)
print(lst)

堆排序

堆排序前传-树

树是一种数据结构 比如:目录结构

树是一种可以递归定义的数据结构

树是由n个节点组成的集合:

  • 如果n=0,那这是一颗空树

  • 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一颗树

一些概念:(参考下图)

  • 根节点、叶子节点 :a就是根节点,bchipqklmn可以理解为叶子节点
  • 树的深度:下图中树的深度为4
  • 树的度:下图中为6,可以理解为叉最多的那个节点的字节点数
  • 孩子节点/父节点:比如E叫做I的父节点,I叫做E的孩子节点
  • 子树:比如这里的E IJP如果单独拿出来就形成了一个子树

时间复杂度:O(nlogn)

堆排序前传-二叉树

二叉树:度不超过2的树

每个节点最多有两个孩子节点

两个孩子节点被区分为左孩子节点和右孩子节点

堆排序前传-完全二叉树

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

完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的节点都集中在该层最左边的若干位置的二叉树。

堆排序前传-二叉树的存储方式

链式存储方式

顺序存储方式

顺序存储方式

堆排序-什么是堆

堆:一种特殊的完全二叉树结构

  • 大根堆:一颗完全二叉树,满足任一节点都比其孩子节点大
  • 小根堆:一颗完全二叉树,满足任一节点都比其孩子节点小

堆排序-堆的向下调整

图1中根节点比子节点要小,不满足一个堆的特点,所以对2这个节点进行向下调整,逐渐向下找合适的数放到合适的节点,形成一个堆(图2)

图1

图2

# 堆的向下调整

def sift(lst, low, height):
    """
    :param lst: 列表
    :param low: 堆的根节点位置
    :param height: 堆的最后一个元素的位置
    :return: 调整好的一个堆(完全二叉树)
    """
    i = low  # i最开始指向根节点的位置
    j = 2 * i + 1  # j开始指向的是i的左孩子节点的位置
    tmp = lst[low]
    while j <= height:  # 如果左孩子的位置<= 最后一个元素的位置
        if j + 1 <= height and lst[j + 1] > lst[j]:  # 如果有右孩子且比左孩子大
            j = j + 1  # j指向了右孩子
        if lst[j] > tmp:  # 如果左孩子大于堆顶元素
            lst[i] = lst[j]
            i = j
            j = 2 * i + 1
        else:
            lst[i] = tmp
    else:
        lst[i] = tmp

堆排序- 构造堆

构造后

# 构造堆
def heap_sort(lst):
    pass
    """
     如果最后一个节点的位置是i,那么父亲节点的位置就是 (i - 1) // 2
     在这里list中最后一个数的位置是i - 1, 所以父节点的位置就是 (i - 2) // 2
    """

    n = len(lst)
    for i in range((n-2)//2, -1, -1):  # i代表建堆的时候调整的位置的根的下标(比如第一次就是图1中的3)
        sift(lst, i, n - 1)
    # 建堆完成

堆排序的过程

# 1.构造堆
def heap_sort(lst):
    """
     如果最后一个节点的位置是i,那么父亲节点的位置就是 (i - 1) // 2
     在这里list中最后一个数的位置是i - 1, 所以父节点的位置就是 (i - 2) // 2
    """
        # 2. 向下调整
    n = len(lst)
    for i in range((n-2)//2, -1, -1):  # i代表建堆的时候调整的位置的根的下标(比如第一次就是图1中的3)
        sift(lst, i, n - 1)
    # 建堆完成

    # 3.挨个出数
    for i in range(n - 1, -1, -1):   # i指的是当前堆的最后一个元素
        lst[0], lst[i] = lst[i], lst[0]
        sift(lst, 0, i - 1)

import random
lst = [i for i in range(11)]
random.shuffle(lst)
print(lst)

heap_sort(lst)
print(lst)

其他:python内置模块

import heapq

heapq.heapify(lst)  # 建堆,建立的是小根堆

heapq.heappop(lst)  # 取出最小的那个数

堆排序-topk问题

现在有n个数,设计算法得到前k大的数。(k<n)

解决思路:

  • 排序后切片 O(nlogn)
  • 排序LowB三人组(若采用冒泡排序,只取前面k个数, 时间复杂度O(kn))
  • 堆排序思路 O(klogn)
    • 取列表前k个元素建立一个小根堆,堆顶就是目前第k大的数
    • 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素,如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整
    • 遍历列表所有元素后,倒序弹出堆顶

使用堆排序实现

将列表中前五个数取出组成一个小根堆,然后…

# 基于建立小根堆的原理修改一下sift函数
def sift(lst, low, high):
    """
    :param lst: 列表
    :param low: 堆的根节点位置
    :param high: 堆的最后一个元素的位置
    :return: 调整好的一个堆(完全二叉树)
    """
    i = low  # i最开始指向根节点的位置
    j = 2 * i + 1  # j开始指向的是i的左孩子节点的位置
    tmp = lst[low]
    while j <= high:  # 如果左孩子的位置<= 最后一个元素的位置
        if j + 1 <= high and lst[j + 1] < lst[j]:  # 如果有右孩子且比左孩子大
            j = j + 1  # j指向了右孩子
        if lst[j] < tmp:  # 如果左孩子大于堆顶元素
            lst[i] = lst[j]
            i = j
            j = 2 * i + 1
        else:
            lst[i] = tmp
            break
    else:
        lst[i] = tmp

def topk(lst, k):
    heap = lst[0: k]
    # 建堆
    for i in range((k-2)//2, -1, -1):
        sift(heap, i, k - 1)
    # 2. 遍历
    for i in range(k, len(lst)-1):
        if lst[i] > heap[0]:  # 如果这个数大于堆顶的数,就替换
            heap[0] = lst[i]
            sift(heap, 0, k - 1)  # 在对前k个数组成的堆排序
    # 3. 挨个出书
    for i in range(k-1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)
    return heap

import random
lst = [i for i in range(11)]
random.shuffle(lst)
print(lst)

print(topk(lst, 5))

归并排序

归并

比较左右两个列表的数,然后将小的数取出来

# 假设左右两边都是有序的
def merge(lst, low, mid, high):
    i = low
    j = mid + 1
    tmp = []
    while i <= mid and j <= high:  # 只要左右两边都有数
        if lst[i] < lst[j]:
            tmp.append(lst[i])
            i += 1
        else:
            tmp.append(lst[j])
            j += 1
    #  上一个while执行完,可以左边或者右边没有数了
    while i <= mid:  # 如果左边有数
        tmp.append(lst[i])
    while j <= high:
        tmp.append(lst[j])  # 如果右边有数
    lst[low: high + 1] = tmp

使用归并

分解:将列表越分越小,直至分成一个元素

终止条件:一个元素是有序的

合并:将两个有序列表归并,列表越来越大

时间复杂度:O(nlogn)

空间复杂度:O(n)

def merge_sort(lst, low, high):
    if low < high:
        mid = (low + high) // 2
        merge_sort(lst, low, mid)
        merge_sort(lst, mid + 1, high)
        merge(lst, low, mid, high)

NB三人组小结

三种排序算法的时间复杂度都是O(nlogn)

一般情况而言,就运行时间而言:

  • 快速排序 < 归并排序 < 堆排序

三种排序算法的缺点:

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

希尔排序

分组

def insert_sort_gap(lst, gap):
    for i in range(gap, len(lst)):
        tmp = lst[i]
        j = i - gap
        while j >=0 and lst[j] > tmp:
            lst[j+gap] = lst[j]
            j -= gap
        lst[j + gap] = tmp

def shell_sort(lst):
    d = len(lst) // 2
    while d >= 1:
        insert_sort_gap(lst, d)
        d //= 2

计数排序

对列表进行排序,已知列表中的数范围都在0-100之内。

时间复杂度:O(n)

def count_sort(lst, max_count=100):
    # 约束条件:需要知道列表中最大的数是多少
    count = [0 for _ in range(max_count + 1)]
    for val in lst:
        count[val] += 1
    lst.clear()
    for index, val in enumerate(count):
        for i in range(val):
            lst.append(index)

import random
lst = [random.randint(0, 100) for _ in range(1000)]
count_sort(lst)
print(lst)

桶排序

# 条件,知道列表中的最大值
def bucket_sort(lst, n=100, max_num=10000):
    buckets = [[] for _ in range(n)]  # 创建n个桶
    for var in lst:  # 判断桶应该放到哪个桶里
        i = min(var // (max_num // n), n - 1)  # i 表示var放到几号桶里
        buckets[i].append(var)
        for j in range(len(buckets[i]) - 1, 0, -1):  # 对桶内的数排序
            if buckets[i][j] < buckets[i][j - 1]:
                buckets[i][j], buckets[i][j-1] = buckets[i][j-1], buckets[i][j],
            else:
                break
    # 输出桶中的元素
    sorted_lst = []
    for buc in buckets:
        sorted_lst.extend(buc)
    return sorted_lst

基数排序

时间复杂度:O(kn)

空间复杂度:O(k+n)

k表示数字位数

import random

def radix_sort(lst):
    max_num = max(lst)  # 最大数 99 ->2, 888 -> 3, 10000 -> 5

    it = 0  # 记录要循环多少次
    while 10 ** it <= max_num:
        buckets = [[] for _ in range(10)]
        for var in lst:
            digit = (var//10**it) % 10
            buckets[digit].append(var)
        lst.clear()
        for buc in buckets:
            lst.extend(buc)
        it += 1
    print(lst)

lst = list(range(1000))
random.shuffle(lst)
radix_sort(lst)

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章