tip:需要提前明白理解递归。
汉诺塔问题源自印度一个古老的传说,印度教的“创造之神”梵天创造世界时做了 3 根金刚石柱,其中的一根柱子上按照从小到大的顺序摞着 64 个黄金圆盘。梵天命令一个叫婆罗门的门徒将所有的圆盘移动到另一个柱子上,移动过程中必须遵守以下规则:
通过分析移动思路,可以总结出一个规律:对于 n 个圆盘,分为a(起始柱),b(辅助柱),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)
树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合。
(A) (B)
图 1 树的示例
结点:使用树结构存储的每一个数据元素都被称为“结点”。
父结点(双亲结点)、子结点和兄弟结点:对于图 1(A)中的结点 A、B、C、D 来说,A 是 B、C、D 结点的父结点(也称为“双亲结点”),而 B、C、D 都是 A 结点的子结点(也称“孩子结点”)。对于 B、C、D 来说,它们都有相同的父结点,所以它们互为兄弟结点
树根结点(简称“根结点”):每一个非空树都有且只有一个被称为根的结点。树根的判断依据为:如果一个结点没有父结点,那么这个结点就是整棵树的根结点。
叶子结点:如果结点没有任何子结点,那么此结点称为叶子结点(叶结点)。
对于一个结点,拥有的子树数(结点有多少分支)称为结点的度(Degree)。 一棵树的度是树内各结点的度的最大值。
满足以下两个条件的树就是二叉树:
如果二叉树中除了叶子结点,每个结点的度都为 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)堆排序在几种效率高的排序中效率低。
这里的空间复杂度是因为递归,递归需要空间。