数据结构与算法
1.平方时间复杂度
----选择排序
def select_sort(items):
"""简单选择排序"""
# items = items[:]
for i in range(len(items) - 1):
min_index = i
for j in range(i + 1, len(items)):
if items[min_index]>items[j]:
min_index = j
items[i], items[min_index] = items[min_index], items[i]
return items
a=[54,74,32,42,27,7,5]
print(select_sort(a))
#output:[5, 7, 27, 32, 42, 54, 74]
----冒泡排序
每次循环,两两比较,最大的数会逐渐上浮到末尾
def bubble_sort(items):
"""冒泡排序"""
for i in range(1,len(items)):#i从1开始到n-1
is_sorted=True #每一次比较开始都设置is_sorted为true,只要交换次序了就说明还没排好序
for j in range(len(items)-i):
if items[j]>items[j+1]:
items[j],items[j+1]=items[j+1],items[j]
is_sorted=False
if is_sorted==True:
break
#当一轮循环过后,isSorted 还是 true,说明此时数组已经排好序了,退出循环即可。
return items
a=[54,74,32,42,27,5,97]
print(bubble_sort(a))
#output:[5, 27, 32, 42, 54, 74, 97]
----搅拌排序(冒泡排序升级版)
双向冒泡排序:奇次循环从头开始两两比较大的下沉,偶次循环从末尾开始两两比较小的上浮。
def bubble_sort(items):
"""搅拌排序(冒泡排序升级版)"""
for i in range(len(items)-1):
exchange_sorted=False
for j in range(len(items)-i-1): #大的上浮
if items[j]>items[j+1]:
items[j],items[j+1]=items[j+1],items[j]
exchange_sorted=True
#print(items)
if exchange_sorted==True:
exchange_sorted = False
for j in range(len(items)-i-1,i,-1): #倒过来比较,小的下沉
if items[j-1]>items[j]:
items[j],items[j-1]=items[j-1],items[j]
exchange_sorted = True
# print(items)
if not exchange_sorted:
break
return items
a=[54,74,32,42,27,5,97,13,53,26]
bubble_sort(a)
#output:[5, 13, 26, 27, 32, 42, 53, 54, 74, 97]
----插入排序
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。类似插书:在书架中先找到相应位置,将后面的书往后推,再将书插入。
def insert_sort(items):
"""直接插入排序"""
for i in range(1,len(items)):#从第二个元素开始比较
element=items[i] #当前元素
j=i-1 #j代表前面的元素
while j>=0:
if items[j]>element:
items[j+1]=items[j]
else:
break
j-=1
items[j+1]=element
#print(items)
return items
a=[54,74,32,42,27,5,97,13,53,26]
print(insert_sort(a))
#output:[5, 13, 26, 27, 32, 42, 53, 54, 74, 97]
2.对数线性时间复杂度
----归并排序(Merge Sort)
思路:分治法,把一个大问题化解为k个中问题,每个中问题再化解为k个小问题,直至问题化为最小可解的问题。对这些问题求解,再不断地合并结果,直至合并完毕。
先假设两个有序的数组,合并成一个有序数组。
def merge_sort(items1,items2):
"""归并排序 先假设用两个有序的数组,合并成一个有序数组。"""
new_items=[]
i=0
j=0
while i<len(items1) and j<len(items2):
if items1[i]<items2[j]:
new_items.append(items1[i])
i+=1
else:
new_items.append(items2[j])
j+=1
return new_items
a=[2,6,7,9]
b=[1,3,4,8]
print(merge_sort(a,b))
#output>>:[1, 2, 3, 4, 6, 7, 8]
如果是无序数据,将其对半切片,直到切割后的数组是有序数组。
#索引切片示例
A=[54,74,32,42,27,5,97,13,53,26]
B=A[:len(A)//2]
C=A[len(A)//2:]
print(B)
print(C)
#>>output:[54, 74, 32, 42, 27]
#>>[5, 97, 13, 53, 26]
完整程序语句如下:
def merge_sort(items1,items2):
"""归并排序 先假设用两个有序的数组,合并成一个有序数组。"""
new_items=[]
i=0
j=0
while i<len(items1) and j<len(items2):
if items1[i]<=items2[j]:
new_items.append(items1[i])
i+=1
else:
new_items.append(items2[j])
j+=1
new_items=new_items+items1[i:] #如果左边指针已经到最后了,当前指针指向超界,返回的是一个空列表
new_items = new_items+items2[j:]
return new_items
def division(items):
"""递归不停地把无序数组切成2半"""
if len(items)<=1: #递归结束语句,拆到只剩一个
return items
mid = len(items)//2
left=division(items[:mid]) #得到一个新的有序列表,一直调用到递归
#结束语句,从[54,74]中切割得到单个数字54,再返回54
right=division(items[mid:])
#得到54后执行这句话,就是从[54,74]中得到74,再返回74
return merge_sort(left,right)
a=[54,74,32,42,27,5,97,13,53,26]
print(division(a))
output:>>[5, 13, 26, 27, 32, 42, 53, 54, 74, 97]
----快速排序
思路:把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
排序的语句思路:
anchor = items[0]
left, right = 0, len(items) - 1
if anchor>items[right]:
items[left]=items[right]
left+=1
elif anchor<items[right]:
right-=1
if anchor>items[left]:
left+=1
elif anchor<items[left]:
items[right]=items[left]
right-=1
上述逻辑构造,真正循环语句重新排列应该为:
def quick_sort(items):
"""快速排序"""
anchor = items[0]
left, right=0, len(items)-1
while left<right: #判断条件,左右指针相遇
while left<right and anchor<=items[right]:
right-=1
# 右边比锚点要大,右指针往左移,也是一个循环,所以要写在while选择里面,不满足while循环则交换
items[left]=items[right]
left+=1
while left<right and anchor>=items[left]:
left+=1
#左指针同理
items[right]=items[left]
right-=1
items[left]=anchor
return items
a=[54,74,32,42,27,5,97,13,53,26]
print(quick_sort(a))
#<<[26, 53, 32, 42, 27, 5, 13, 54, 97, 74]
这是一层判断,调用递归对排完序的子序列进行多次循环判断。
def quick_sort(items,first,last): #递归时,子序列的最左边和最右边是动态的,通过传值改变
"""快速排序"""
if first>=last:
return
anchor = items[first]
left, right=first, last
while left<right: #判断条件,左右指针相遇
while left<right and anchor<=items[right]:
right-=1
# 右边比锚点要大,右指针往左移,也是一个循环,所以要写在while选择里面,
# 不满足while循环则交换,不改变指针,以免出现交错。
items[left]=items[right]
#left+=1 可以不要这步,下面循环里实行
while left<right and anchor>=items[left]:
left+=1
#左指针同理
items[right]=items[left]
#right-=1 可以不要这步
items[left]=anchor
quick_sort(items,first,left-1)
quick_sort(items,left+1,last)
return items
if __name__=='__main__':
a=[54,74,32,42,27,5,97,13,53,26]
print(a)
quick_sort(a,0,len(a)-1)
print(a)
#>>[54, 74, 32, 42, 27, 5, 97, 13, 53, 26]
#>>[5, 13, 26, 27, 32, 42, 53, 54, 74, 97]
3.对数时间复杂度
----折半查找
二分查找:有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
def bin_search(items,element):
start=0
end=len(items)-1
mid = (start + end) // 2
while element!=items[mid]:
while start==mid:
return -1
mid = (start + end) // 2
if element>items[mid]:
start=mid
else:
end=mid
return mid
a=[5, 13, 26, 27, 32, 42, 53, 54, 74, 97]
print(bin_search(a,45))
>>-1
4.贪婪算法
当一个问题的最优解包含其子问题的最优解时,它可能是动态规划问题或者是贪心算法问题。
若整体最优解可以通过一系列局部最优的选择达到,则为贪心算法问题。
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望得到结果是最好或最优的算法。
比如付钱,在纸币硬币都较多情况下,先把大的纸币付了,再付小的,直到凑齐。
常见贪心算法:单元最短路径、最小生成树、哈夫曼编码
5. 动态规划
---- 以股票算法题为例:
买卖股票的最佳时机 I
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
假设某股票7天的股价为: 5 3 2 7 4 8 4,在哪天买入哪天卖出利润最大。
一开始的思路是2层循环,先找到每天的最大股价;再比较差价中的最大值。比较出每天的最大值后还需要对乱序数组进行冒泡法找到最大数。
最简单的思路是:每天进行比较到当天为止,买入卖出的最大价格。
————转移方程
对于买来说,买之后可以卖出(进入卖状态),也可以不再进行股票交易(保持买状态)。
对于卖来说,卖出股票后不在进行股票交易(还在卖状态)。
只有在手上的钱才算钱,手上的钱购买当天的股票后相当于亏损。也就是说当天买的话意味着损失-prices[i],当天卖的话意味着增加prices[i],当天卖出总的收益就是 buy+prices[i] 。
buy=当天买和以前买的最大数;
sell=当天卖和以前卖的最大数
def maxProfit(prices):
if(len(prices)<= 1):
return 0
buy = -prices[0]
sell=0
for i in range(1,len(prices)):
buy =max(buy, -prices[i]) #以前买和当天买
print(buy)
sell =max(sell,prices[i]+buy) #买入的值影响到卖,以前卖和现在卖
print(sell)
return sell
a=[4,3,2,7,4,8,4]
print(a)
print(maxProfit(a))
*** 买卖股票的最佳时机 II***
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:手上只能有一只股票,你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
————转移方程
对比上题,这里可以有无限次的买入和卖出,也就是说 买入 状态之前可拥有 卖出 状态,所以买入的转移方程需要变化。
buy = max(buy, sell - price[i])
sell = max(sell, buy + prices[i] )
def maxProfit(prices):
if(len(prices)<= 1):
return 0
buy = -prices[0]
sell=0
for i in range(1,len(prices)):
sell =max(sell, prices[i] + buy)
print(sell)
buy = max(buy, -prices[i] + sell)
print(buy)
return sell
a=[4,3,2,7,4,8,4]
print(a)
print(maxProfit(a))
*** 买卖股票的最佳时机 IlI***
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票。
状态
有 第一次买入(fstBuy) 、 第一次卖出(fstSell)、第二次买入(secBuy) 和 第二次卖出(secSell) 这四种状态。
转移方程
这里最多两次买入和两次卖出,也就是说 买入 状态之前可拥有 卖出 状态,卖出 状态之前可拥有 买入 状态,所以买入和卖出的转移方程都需要变化。
fstBuy = max(fstBuy , -price[i])
fstSell = max(fstSell,fstBuy + prices[i] )
secBuy = max(secBuy ,fstSell -price[i]) (受第一次卖出状态的影响)
secSell = max(secSell ,secBuy + prices[i] )
边界
一开始 fstBuy = -prices[0]
买入后直接卖出,fstSell = 0
买入后再卖出再买入,secBuy - prices[0]
买入后再卖出再买入再卖出,secSell = 0
最后返回 secSell 。
代码没有成功实现
*** 子列表元素之和的最大值***
说明:子列表指的是列表中索引(下标)连续的元素构成的列表;列表中的元素是int类型,可能包含正整数、0、负整数;程序输入列表中的元素,输出子列表元素求和的最大值,例如:
输入:1 -2 3 5 -3 2
输出:8
输入:0 -2 3 5 -1 2
输出:9
输入:-9 -2 -3 -5 -3
输出:-2
def total_max(array):
"""列表中连续元素之和最大值"""
cumu=array[0]
for i in range(1,len(array)):
cumu=max(cumu+array[i],array[i])
print(cumu)
return cumu
还要考虑一层,如果array[i]自己是负数的话,就不需要加上它
正确代码:
def total_max(array):
"""列表中连续元素之和最大值"""
sum=cumu=array[0]
for i in range(1,len(array)):
sum=max(0,sum) #看前面的序列之和是不是正数,是的话加上,不是就不加
sum+=array[i]
cumu=max(cumu,sum) #比较原来的最大值与累计值大小
print(cumu)
return cumu
# a=[1,-2,3,5,-3,2]
# b=[0,-2,3,5,-1,2]
# c=[-9,-2,-3,-5,-3]
# print(total_max(c))
6.深度优先搜索(DFS)
深度优先遍历图算法步骤:
访问顶点v;
依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
DFS 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点 w1;再从 w1 出发,访问与 w1邻 接但还没有访问过的顶点 w2;然后再从 w2 出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止。
接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。