python算法

令人头疼的算法

Posted by QQL on October 4, 2018

python算法实现

算法时间复杂度主要看:一般情况和最坏情况

二分查找

def binary_search(li, val):
    """
    :param li:传入列表
    :param val: 需要查找的值
    :return: val
    """
    left = 0  # 指针left:起始最小长度为0
    right = len(li) - 1  # 指针right:最大长度为列表长度-1
    while left <= right:  # 指针left<=right时说明列表有1~2个值
        mid = (left + right) // 2  # 找到中间值
        if li[mid] > val:  # 二分出来的值与待查找的值进行比较
            right = mid - 1  # right指针移动到mid处-1的位置,即mid左边一位
        elif li[mid] < val:
            left = mid + 1  # left指针移动到mid处+1的位置,即mid右边一位
        else:  # val=mid
            return mid  #拿到找到的值
    else:  # 指针left>right
    时
        return None  # 列表没有需要查找的值

lowb三人组(时间复杂度O(n^2))

冒泡排序

原理:冒泡排序,长度为n的列表,走n-1趟
每走一趟,把无序区最大的数放在最后,即冒泡,并把无序区的其他比较大的数,稍微往上移了一些
指针走到n-1停下,因为n为冒泡的数,之后重新回到无序区0的位置
走i趟,则指针在n-1-i(-1:指针每次都少走一位,因为n为要冒泡的值;-i:第i趟,就已经有i个冒泡的值)

def bubble_sort_plus(li):
    for i in range(len(li) - 1):
        # i表示第i趟 有序区有i个数
        exchange = False  # 标识符为False
        for j in range(len(li) - i - 1):  # 循环无序区
            if li[j] > li[j + 1]:  # 如果指针j大于j+1,则需要移动位置
                li[j], li[j + 1] = li[j + 1], li[j]  # 当前位置j替换成j+1,j+1替换成j
                exchange = True  # 如果有交换位置则改为True
        if not exchange:  # 如果走完一趟,依然为False
            return  # 则说明这一趟不改变顺序,为有序,减少循环次数

执行结果: li = [2,5,4,8,7,6,9,1,3]

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

总结:冒泡排序时间复杂度O(n**2)速度太慢
如果冒泡执行一趟没有进行交换,则已经是排好序的列表,直接结束算法

选择排序

原理:选择排序:遍历列表,找到最小的数,放在首位

def select_sort(li):
    for i in range(len(li)-1):
        # 第i趟:有序区li[0:i] 无序区li[i:n],第一趟就位i=0的索引值
        min_loc = i  # 当前循环无序区最小的值,第一次为索引0
        for j in range(i+1, len(li)):  # 循环无序区
            if li[min_loc] > li[j]:  # 当前有序区的最大值对比无序区的值
                min_loc = j  # 下标交换
        li[min_loc], li[i] = li[i], li[min_loc]  # 位置进行交换,相当于li[j]与li[i]进行交换
        print(li)

执行结果:li = [2,5,4,8,7,6,9,1,3]

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

插入排序

原理:扑克牌排序
每次从无序区选择一个元素插入有序区(此方法为相对有序),直至无序区变空
一共有n张牌,开始有一张,一共模n-1次牌

def insert_sort(li):
    for i in range(1, len(li)):  # i既表示趟数,也表示摸到的牌的下标
        j = i - 1  # j指手里的最后一张牌的下标,第一趟只有一张牌,即列表第一个值,下标为0
        tmp = li[i]  # 在无序区新拿到的值
        print(tmp)
        print(li[j])
        while j >= 0 and li[j] > tmp:  # 当手上的牌还有牌大于新的牌 and 当手上最右边的牌大于新摸到的牌tmp
	        # while循环的作用:找插入位置
            li[j + 1] = li[j]  # j指针的位置(手上最右边的牌)向右移一个单位
            j -= 1  # 指针向左移一个单位,找手上从右往左第二个值,再循环进行比较
        li[j + 1] = tmp  # 大于新摸到的牌,全部向右移一个单位,下标j+1(新摸得牌的下标)处为新的数
        print(li)

执行结果:li = [2,5,4,8,7,6,9,1,3]

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

一般电脑一秒钟运算10^7=一千万次,可以估算时间复杂度的实际时间

牛逼三人组

快速排序

原理:取一个元素P(第一个元素),使元素P归位(让p元素,左边比p小,右边比p大)
列表被p分成两部分,左边比p小,右边比p大
递归完成排序
注意不能给递归函数加装饰器,会导致执行n次装饰器内容,则可对递归函数进行封装优化

def _quick_sort(li, left, right):
    """
    快排
    :param li: 需要排序的列表
    :param left: 左边第一个下标0
    :param right: 右边第一个下标len(li)-1
    :return:
    """
    if left < right:  # left=right一个元素,left>right没有元素
        mid = partition(li, left, right)  # 调用函数返回归位元素下标
        # 递归归位元素两侧
        _quick_sort(li, left, mid - 1)  # mid左边
        _quick_sort(li, mid + 1, right)  # mid右边

def partition(li, left, right):
    ########最坏情况优化方案#########
    # 原本是找第一个数,li[left]进行从右往左的查找
    i = random.randint(left, right)  # 随机从列表中找一个数的下标
    li[left], li[i] = li[i], li[left]  # 让第一个数li[left]交换成随机选择的数li[i]
    ###############################
    tmp = li[left]  # 拿到左边第一个元素
    while left < right:  # 当游标left<right时一直循环,left=right找到mid
        # 从右边找比tmp小的数,因为左边有空位(把tmp拿出来),比tmp小的数放在左边
        while left < right and li[right] >= tmp:  # 当右边的数一直大于tmp时,则会一直遍历找到比tmp小的值停止
            #需要添加left<right防止右边的数一直大于左边的数而无法退出循环,当left=right时说明右边全部值大于tmp
            right -= 1  # 往左走一步
        li[left] = li[right]  # 找到右边的值小于tmp的,填写到左边空位
        # 从左边找比tmp大的数,因为右边有空位,比tmp大的数放到右边
        while left < right and li[left] <= tmp:  # 从左边找比tmp大的值
            left += 1
        li[right] = li[left]  # 把左边的值写到右边的空位上
    li[left] = tmp  # 把tmp归位,即跳出大的循环,left=right使左边小于tmp,右边大于tmp
    return left  # 返回mid值,left=right=mid

时间复杂度O(nlogn) = 有logn层(深度),每层时间复杂度为n(从左到右,从右到左遍历)
快速排序问题:
最坏情况:倒序排序 9 8 7 6 5 4 3 2 1 复杂度为n^2
递归:设置递归深度
解决的方式是手工设置递归调用深度,方式为:

import sys
sys.setrecursionlimit(1000000) #例如这里设置为一百万

堆排序

学前必备知识


1.树是一种数据结构,比如:目录结构
2.树是一种可以递归定义的数据结构
3.树是由n个节点组成的集合:
 * 如果n=0,那这是一颗空树;
 * 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一颗树(子树)
4.一些概念
 * 根节点、叶子节点(不能分叉的节点)
 * 树的深度(高度),看节点数
 * 树的度:是整个树里节点度数(节点的度就是看分了几个叉)最大的度
 * 孩子的节点/父节点
 * 子树
二叉树
1.二叉树:就是度不超过2的数,即每个节点最多分2个叉
2.每个节点最多有两个孩子节点,因为度不超过2
3.两个孩子节点被区分为左孩子节点和右孩子节点
4.满二叉树:一个二叉树,如果每一个层的节点数都达到最大值,即度数都为2,则这个二叉树称为满二叉树
满二叉树.png
5.完全二叉树:叶子节点只能出现在最下层和次下层,并且最下面一层的节点都集中在该层最左边的若干位置的二叉树
完全二叉树.png
6.二叉树的存储方式
 * 链式存储方式
 * 顺序存储方式(堆排序使用顺序存储,即列表
父子.png
堆:一种特殊的完全二叉树结构
 * 大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
大.png
 * 小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
小.png
堆的向下调整:当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变换成一个堆
堆排序的过程
1.建立一个堆(从最后一个非叶子结点进行排序,慢慢扩大范围,直到整个堆)
2.得到堆顶元素,为最大元素
3.去掉堆顶,将堆最后一个元素(最后一个非叶子结点)放到堆顶,此时可以通过调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤3,直到堆变空

def sift(li, low, high):
    """
    向下调整函数的实现
    :param li: 列表
    :param low: 堆的根节点位置
    :param high: 堆的最后一个元素的位置
    :return:
    """
    i = low  # i最开始指向根节点
    j = 2 * i + 1  # j开始指向左孩子
    tmp = li[low]  # 把堆顶保存起来,以便向下调整
    while j <= high:  # 只要j位置有数,即小于或等于最后一个元素
        if j + 1 <= high and li[j + 1] > li[j]:  # 如果有右孩子且右孩子>左孩子
            # 用于判断j指向左右哪边
            j = j + 1  # j指向右孩子
        if li[j] > tmp:  # 比堆顶大
            li[i] = li[j]  # 新的堆顶为li[j]
            # 往下看一层,i和j向下移一层
            i = j  # i向下移到j
            j = 2 * i + 1  # j向下移到i的左孩子节点
        else:  # tmp更大,则把tmp放在i位置
            li[i] = tmp  # 把tmp放在某一个领导(非叶子结点)位置上
            break
    else:
        li[i] = tmp  # 如果j>high,把tmp放在叶子节点上

# sift时间复杂度为logn,因为最多走树的高度logn,不是走左边就是走右边
"""
子节点---(n-1)//2--->父节点
列表最后一个元素索引len(n)-1,其中len(n)=n
"""


def heap_sort(li):
    """
    建立一个堆
    (从最后一个非叶子结点进行排序,慢慢扩大范围,直到整个堆)
    :param li:
    :return:
    """
    n = len(li)
    # 最后一个非叶子结点(n-1-1)//2
    for i in range((n - 2) // 2, -1, -1):  # 参数1:range左范围,非叶子结点索引/参数2:range的右范围,即到索引为0的第一个节点,右开特效所以为-1/参数3:步长,倒序为-1
        # i代表建堆时调整的部分根的索引
        sift(li, i, n-1)  # i:根索引=指针low,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)  # i-1是新的high

时间复杂度O(nlogn)
python内置了import heapq堆模块,可以更简单的实现堆
堆排序应用topk问题
现在有n个数,设计算法得到前k大的数。(k<n),类似场景:热搜榜前十
解决思路:

  • 取列表前k个元素建立一个小根堆(堆顶数最小,从下到上,主键减小)。堆顶就是目前第k大的数。
  • 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行调整;
  • 遍历列表所有元素后,倒序弹出堆顶
def sift(li, low, high):
    i = low  # i最开始指向根节点
    j = 2 * i + 1  # j开始指向左孩子
    tmp = li[low]  # 把堆顶保存起来,以便向下调整
    while j <= high:  # 只要j位置有数,即小于或等于最后一个元素
        if j + 1 <= high and li[j + 1] < li[j]:  # 如果有右孩子且右孩子<左孩子
            j = j + 1  # j指向右孩子
        if li[j] < tmp:  # 比堆顶小
            li[i] = li[j]  # 新的堆顶为li[j]
            # 往下看一层,i和j向下移一层
            i = j  # i向下移到j
            j = 2 * i + 1  # j向下移到i的左孩子节点
        else:  # tmp更大,则把tmp放在i位置
            li[i] = tmp  # 把tmp放在某一个领导(非叶子结点)位置上
            break
    else:
        li[i] = tmp  # 如果j>high,把tmp放在叶子节点上


def topk(li,k):
    heap = li[0:k]
    for i in range((k-2)//2, -1, -1):
        sift(heap, i, k-1)

    # 1.建堆
    for i in range(k, len(li)-1):
        if li[i] > heap[0]:
            heap[0] = li[i]
            sift(heap, 0, k-1)

    # 2.遍历
    for i in range(k-1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i-1)

    # 3.出数
    return heap
    
li = list(range(100))
import random
random.shuffle(li)
print(li)
print(topk(li, 10))

归并排序

归并排序演示.gif
归并排序涉及到了递归
时间复杂度:O(nlogn)
空间复杂度:O(n)
python内部的sort基于归并排序

def merge(li, low, mid, high):
    """
    归并的前提:列表分为两段有序
    :param li:
    :param low: 最左指针
    :param mid: 随意的中间值的索引
    :param high: 最右指针
    :return:
    """
    i = low
    j = mid+1
    ltmp = []
    while i<=mid and j<=high:  # 左右两边都有数
        if li[i]<li[j]:  # i指针小于j指针的数
            ltmp.append(li[i])  # 把小的数加到新的列表中
            i+=1
        else:
            ltmp.append(li[j])
            j+=1
    # while执行完,有一部分没值了
    while i<=mid:
        ltmp.append(li[i])
        i+=1
    while j <=high:
        ltmp.append(li[j])
        j += 1
    li[low:high+1] = ltmp  # 新建的列表写回原列表


def merge_sort(li, low, high):
    """
    归并排序
    :param li: 
    :param low: 最小值指针
    :param high: 最大值指针
    :return: 
    """
    if low<high:  # 至少有两个元素,递归
        mid = (low+high)//2  # mid经过递归,每次都在减半,直到减到1时,跳出进行归并
        merge_sort(li, low, mid)
        merge_sort(li, mid+1, high)
        merge(li, low, mid, high)

NB三人组总结

1.时间复杂度都是O(nlogn)
2.速度:快速排序>归并排序>堆排序 3.缺点
 * 快速排序:极端情况下排序效率低(倒序)
 * 归并排序:需要额外的内存开销(开辟一个新的列表ltmp)
 * 堆排序:在NB三人组中,排序算法相对较慢
小结.png
稳定性

  • 稳定排序:相同的元素进行排序,相同元素的原本位置前后顺序不会改变。在排序中前后交换位置都为稳定
  • 不稳定排序:在排序中交换位置,飞着交换(跨过其他元素)都为不稳定