Python的几个基础算法

tip:需要提前明白理解递归。

汉诺塔经典问题

汉诺塔问题源自印度一个古老的传说,印度教的“创造之神”梵天创造世界时做了 3 根金刚石柱,其中的一根柱子上按照从小到大的顺序摞着 64 个黄金圆盘。梵天命令一个叫婆罗门的门徒将所有的圆盘移动到另一个柱子上,移动过程中必须遵守以下规则:

  • 每次只能移动柱子最顶端的一个圆盘;
  • 每个柱子上,小圆盘永远要位于大圆盘之上;

通过分析移动思路,可以总结出一个规律:对于 n 个圆盘,分为a(起始柱),b(辅助柱),c三根柱子的汉诺塔问题,移动圆盘的过程是:

  1. 将起始柱a上的 n-1 个b通过c移动到b上;
  2. 将起始柱a上遗留的 第n个圆盘移动到c上;
  3. 将辅助柱b上的所有圆盘通过a移动到目标柱c上。

下面进行代码复现:

def hannuo(n,a,b,c):
    if n>0:
           hannuo(n-1,a,b,c)
           print("moving from %s to %s "%(a,c))
           hannuo(n-1,b,a,c)


hannuo(3,'A','B','C')

运行结果: 

moving from  A  to  C
moving from  A  to  C
moving from  B  to  C
moving from  A  to  C
moving from  B  to  C
moving from  B  to  C
moving from  A  to  C 

 查找算法

查找:用一定的方法,查找与给定关键字相同的数据元素的过程。

列表查找

输入需要查找的列表和元素,输出下标。没有则返回none,或-1.------index()函数

顺序查找

从第一个元素开始搜索,直到找到元素,或者直达列表最后一个元素。

代码复现:

del line_search(li,val):
    for ind,v in enumerate(li):
       if v==val:
           return ind
    else:
        return None

简单来说就是遍历,直至找到该元素。

时间复杂度:O(n)

二分查找

前提是排序,通过与中间值比较大小得出相应区间,然后再次二分,与二分法找零点道理相似。

代码复现:

def binary_search(li,val):
    left=0
    right=len(li)-1
    while left<= right:  #确定选区有值
        mid = (left+right)//2
        if li(mid) == val:
            return mid
        elif li(mid) > val:
            right=mid-1  #操作后会进入下一个循环
        else:
            left=mid+1
    else:
        return None

这里的循环虽然没有像之前举例一样,有明显的次数减半的操作,但是通过对算法的理解,我们能明白这是每次减半的过程,所以

时间复杂度:O(logn)

排序算法

将无序数列,组合成有序数列的过程叫做排序。

列表排序 

输入列表,输出有序列表。----内置函数sort()

冒泡排序

过程:通过比大小不断地交换 前后的值,直至形成一个有序数列。每次循环结束,有序区增加一位,无序区减少一位

利用代码复现,从最坏情况中举个例子。

假设四个,且顺序为完全导致。

那么第一次:

[4, 3, 2, 1]
#第一次排序,从4开始
[3, 4, 2, 1]
[3, 2, 4, 1]
[3, 2, 1, 4]
#完成了对4的排序,开始第二次排序
[2, 3, 1, 4]
[2, 1, 3, 4]
#完成了对3的排序,开始第三次排序
[1, 2, 3, 4]
#此时,第三次排序本质是对2进行排序,但对2排完序即最后一个元素也相应的归位了

则冒泡排序需要外循环n-1次。内循环(假设我们有n个元素) ,那么代码复现:

def bubble_sort(li):
    for i in range(len(li)-1):
        for j in range(len(li)-i-1):
            if li[j]>li[j+1]:
                li[j],li[j+1]=li[j+1],li[j]
                print(li)

时间复杂度:O(n^2)

当然,冒泡排序也可以优化,如没有发生交换则证明有序可以停止进程。

def bubble_sort(li):
    for i in range(len(li)-1):
        exchange=False
        for j in range(len(li)-i-1):
            if li[j]>li[j+1]:
                li[j],li[j+1]=li[j+1],li[j]
                exchange=True
        if not exchange:
            return  

选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置(或新序列),然后再从剩余的未排序元素中寻找到最小(大)元素(或新序列),然后放到已排序的序列的末尾。

def select_sort(li):
    li_new=[]
    for i in range(len(li)):
        min_val=min(li)
        li_new.append(min_val)
        li.remove(min_val)
    return li_new

虽然代码简单,但必须要注意的是,使用的函数min本身也在遍历列表。

时间复杂度:需要考虑寻找min的方法复杂度。

插入排序

假设有序区为第一个数(一张牌),此时不断地从无序区抽取新的数,放到有序区的正确位置(抽牌并插入正确位置),知道无序区没有数字(没牌了)完成排序。

def insert_sort(li):
    for i in range(1,len(li)):#默认第一个数为有序区
        tmp=li[i]
        j=i-1 #有序区的最后一个数,也是最大的一个数
        while j>=0 and tmp < li[j]:
            #此时抽到的牌比最大的牌小,若比有序区最大值大则不进行操作,只将小的值向前排序
            li[j+1]=li[j] #已知牌向后推移,给抽到的牌让位
            li[j]=tmp #在原来的位置用更小的赋值
            j-=1  #向前移,直到没有数比选定值大

时间复杂度:O(n^2)

快速排序

思路:1)选定值,将其快速排到正确位置。

2)正确位置的值使得无序数列被分为了两部分小的无序数列

3)递归,直至完成排序

代码复现:

def partition(li,left,right):
    tmp=li[left]
    while left=tmp:#从最右边寻找小于tmp的数
            right-=1 #right左移直至退出循环(找到小于tmp的值或者与left相等)
        li[left]=li[right]
        while left

利用partition函数完成对选定值的定位,利用quick_sort递归调用。

此时的时间复杂度,不能单单从几次循环来判断,要深刻的理解算法的过程。 推荐下面这个·链接,讲得非常清晰。

需要理解,快速排序效率提高的来源是什么。

(26条消息) 如何理解快速排序的时间复杂度是O(nlogn)_sun123704的博客-CSDN博客_快速排序时间复杂度为什么是nlogn

一般来说,时间复杂度:O(nlogn) 

堆排序

树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。

Python的几个基础算法_第1张图片

                                                                 

       (A)                                                                        (B) 

 1 树的示例

树的结点

结点:使用树结构存储的每一个数据元素都被称为“结点”。

父结点(双亲结点)、子结点和兄弟结点:对于图 1(A)中的结点 A、B、C、D 来说,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点

 树根结点(简称“根结点”):每一个非空树都有且只有一个被称为根的结点。树根的判断依据为:如果一个结点没有父结点,那么这个结点就是整棵树的根结点。

叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。

结点的度 

对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)。 一棵树的度是树内各结点的度的最大值。

二叉树

满足以下两个条件的树就是二叉树:

  1. 本身是有序树;
  2. 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;

满二叉树

如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。

完全二叉树

如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。

二叉树的顺序存储结构

 指的是利用列表储存二叉树。仅需从根节点开始,按照层次依次将树中节点存储到数组即可。

假设现在一棵非完全二叉树,拿一棵普通的二叉树举例,一棵普通二叉树有5种形态(空树、只有根结点、只有左子树、只有右子树、左右子树都有),从形态上来看可能是一棵“残缺不全”的二叉树,如果从根结点开始从1 挨个编号,然后在存进一维数组中,那么有些结点可能没有孩子,那么它原本的孩子在数组中的位置就会被后面上来的的结点占据,这样就无法通过下标寻找相应的规律。

只有完全二叉树才可以使用顺序表存储。

将父节点设为i,则左孩子结点为2i+1,右孩子为2i+2.

大根堆: 若根节点存在左右子节点,那么根节点的值大于或等于左右子节点的值。
小根堆: 若根节点存在左右子节点,那么根节点的值小于或等于左右子节点的值。

堆排序最重要的是两个部分:向下调整和创造堆。

具体过程不描述了,太繁琐。

代码复现:

def sift(li,low,high): #完成向下调整
    #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
        if li[j] > tmp: #将孩子与父亲比较,如果孩子大
            li[i] = li[j] #孩子上移
            i=j
            j=2*i+1
        else:   #父亲更大,停止交换
            break
    else:
        li[i]=tmp #将根结点的值储存回去

def heap_sort(li):#构建堆
    n=len(li)
    for i in range((n-2)//2,-1,-1):#从最后一个叶子结点开始构造子树,完成大根堆
        sift(li,i,n-1)
    for i in range(n-1,-1,-1): #将找出来的最大数存到下面,不浪费新的内存空间
        li[0],li[i] = li[i],li[0] #这是最重要的一步!!!按序输出的根本
        sift(li,0,i-1)#此时high永远是最后一层,不需要考虑其他子树
        

时间复杂度:O(nlogn)--不要深究,理解为主。

归并排序

将两个有序列表通过归并合成新列表。

通过比较有序列表的相同位,将更大(小)的元素储存在新列表中。

def merge(li,low,mid,high):
    i=low
    j=mid+1
    ltmp=[]
    while i<=mid and j<=high:
        if li[i]

注意:这里的思想最重要的是理解一个元素是有序,即每两个元素都能使用一次归并。

时间复杂度:有减半的过程,且每个元素都便利了,O(nlogn)

空间复杂度:O(n)——存在了新列表里。

总结(快排,堆排,归并)

时间复杂度:O(nlogn)

一般来说:快速排序<归并排序<堆排序

1)在极端条件下,快排时间长,效率低。

2)归并排序开辟了新的储存空间。

3)堆排序在几种效率高的排序中效率低。

Python的几个基础算法_第2张图片

 这里的空间复杂度是因为递归,递归需要空间。

你可能感兴趣的:(算法)