算法学习笔记1 递归、查找(顺序,二分)、排序(冒泡,选择,插入,快速,堆排序)

如何简单快速地判断算法复杂度:

在这里插入图片描述

递归

在这里插入图片描述

def func1(x):
	print(x)
	func1(x-1)

def func2(x):
	if x>0:
		print(x)
		func2(x+1)

虽然上面两个函数都调用了自身,但由于没有结束条件,故不是标准地递归。

def func3(x):
	if x>0:
		print(x)
		func3(x-1)

def func4(x):
	if x>0:
		func4(x-1)
		print(x)

func3(3)输出为3 2 1,执行过程为先输出再调用递归函数,流程如下图所示:
在这里插入图片描述
func4(3)输出为1 2 3,执行过程为先调用递归函数再输出,流程如下图所示:
在这里插入图片描述

递归实例:汉诺塔问题

在这里插入图片描述
代码实现:

def hanoi(n,a,b,c):
	if n>0:
		hanoi(n-1,a,c,b)
		print("moving from %s to %s" %(a,c))
		hanoi(n-1,b,a,c)
hanoi(3,'A','B','C')

查找

在这里插入图片描述

顺序查找

在这里插入图片描述

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

二分查找-实例

在这里插入图片描述
mid所指的5比要查找的元素3大,故right指针所指的位置变为mid-1,此时mid指向的位置变为(0+3)//2=1。
在这里插入图片描述
mid所指的2比要查找的元素3小,故left指针所指的位置变为mid+1,此时mid指向的位置变为(2+3)//2=2。
在这里插入图片描述
如果位置2的元素不是3而是4,那么right将跑到left的左边,所以当right<left时说明列表没有我们要找的元素。
在这里插入图片描述
代码实现:

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:	# 待查找的值在mid左侧
			right = mid - 1
		else:	# li[mid]<val 待查找的值在mid右侧
			left = mid + 1
	else:
		return None

二分查找有一个while循环,但是循环减半的,故复杂度为O(logn)
在这里插入图片描述
内置列表查找函数:index()属于顺序查找,因为二分查找要求列表必须是有序的,而我们的列表不一定总是有序的。先进行排序再二分查找所用的时间可能比直接用顺序查找的时间要长,所以如果遇上无序列表,我们只需要查找一次,那么可以直接使用顺序查找;如果是后面需要多次用到查找,可以先排序,后面就可以用二分查找了。


排序

在这里插入图片描述
常见排序算法:
在这里插入图片描述

冒泡排序(Bubble Sort)在这里插入图片描述

在这里插入图片描述
先比较第0位置所指的7和它后面一个元素5,7比5大,交换两个数的位置。以此类推,完成一趟排序后,最大的元素9被放到了最后。
在这里插入图片描述
经过n-1趟后,列表变为有序
在这里插入图片描述
代码实现:

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

两层循环,算法的时间复杂度为O(n^2)
在这里插入图片描述
冒泡排序还有可以改进的地方,如果我们传入的列表已经是有序的列表,用上面的代码还是要执行n趟。我们可以在代码中定义,如果执行一趟后没有交换任何数,说明列表已经有序,直接退出循环。

# 冒泡排序改进版
def bubble_sort(li):
	for i in range(len(li)-1):	# 第i趟
		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
		print(li)
		if not exchange:
			return
				

选择排序(Select Sort)

在这里插入图片描述
select_sort_simple()实现方式可以很直观的理解选择排序的思想,但不建议使用。因为新建一个列表保存取出来的最小值增加了内存空间,并且min()和remove操作时间复杂度都是n。选择排序select_sort()有两层循环,时间复杂度为O(n^2)。

def select_sort_simple(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

def select_sort(li):
    for i in range(len(li)-1): # i是第几趟
        min_loc = i
        for j in range(i+1, len(li)):
            if li[j] < li[min_loc]:
                min_loc = j
        li[i], li[min_loc] = li[min_loc], li[i]
        print(li)

插入排序

在这里插入图片描述

def insert_sort(li):
	for i in range(1, len(li)):	# i表示摸到的牌的下标
		tmp = li[i]
		j = i - 1	# j指的是手里的牌的下标
		while j>=0 and li[j] > tmp:
			li[j+1] = li[j]
			j-=1
		li[j+1] = tmp
		print(li)

li=[3,2,4,1,5,7,9,6,8]
print(li)
insert_sort(li)

j>=0结束循环是考虑到当抽出来的元素3比前面的有序元素都小,指针j会一直往左移动,当j=-1时结束循环,将抽出的元素3插到j+1位置,作为第一个元素。如果li[j]<=tmp,直接退出循环,将抽出的元素插到li[j+1]处。
在这里插入图片描述
在这里插入图片描述

快速排序

在这里插入图片描述
在这里插入图片描述
假设我们归位的过程可以用partition()函数完成,快速排序整体框架如下:

def quick_sort(data, left, right):
    if left<right:  # 至少两个元素
        mid = partition(data, left, right)
        quick_sort(data, left, mid-1)
        quick_sort(data, mid+1, right)

归位完成过程图示:
在这里插入图片描述
将第一位元素5取出来,左边留下一个空位,从右边取一个小于5的元素放到左边的空位处。此时右边又空出来一位,改从左边取一个大于5的元素放到右边的空位处。以此类推,当左右指针重合,则将元素5放到指针所指位置。
在这里插入图片描述
代码实现:

def partition(li, left, right):
    tmp = li[left]
    while left < right:
        while left < right and li[right] >= tmp: #从右面找比tmp小的数
            right -= 1      # 往左走一步
        li[left] = li[right] #把右边的值写到左边空位上
        # print(li, 'right')
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left] #把左边的值写到右边空位上
        # print(li, 'left')
    li[left] = tmp      # 把tmp归位
    return left


def _quick_sort(li, left, right):
    if left<right:  # 至少两个元素
        mid = partition(li, left, right)
        _quick_sort(li, left, mid-1)
        _quick_sort(li, mid+1, right)

def quick_sort(li):
    _quick_sort(li, 0, len(li)-1)

在这里插入图片描述
归位过程指针分别从左右两侧移到重合处,复杂度为O(n),总共有logn次递归,故时间复杂度为O(nlogn)。
在这里插入图片描述
最坏的情况为传入的列表是倒序的,一次归位,将最大的元素放到最右边,递归只有左边,每次递归都只有左边,不是折半分,故时间复杂度会变为O(n^2)。
python默认的递归深度是很有限的,大概是900多的样子,当递归深度超过这个值的时候,就会引发这样的一个异常:RuntimeError: maximum recursion depth exceeded

解决的方式是手工设置递归调用深度,方式为:

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

堆排序

堆排序前传——树与二叉树

在这里插入图片描述
树的节点就相当于列表中的元素。在列表中,我们称为元素;在树结构中,我们把树的每个元素称为节点。
在这里插入图片描述

  1. 处在树的最顶端(没有双亲)的结点叫根节点;叶子节点就是树中最底段的节点,叶子节点没有子节点。
  2. 定义一棵树的根节点层次为1,其他节点的层次是其父节点层次加1。一棵树中所有节点的层次的最大值称为这棵树的深度,树的深度即看树结构有多少层。
  3. 节点的度为其孩子节点的个数,即节点分了几个杈。一棵树中,最大的节点的度称为树的度。如上图中的树,根节点A的度最大,分了6个杈,故树的度为6。
  4. 以前面的树为例,E是I的父节点,I是E的孩子节点。
  5. 将E,I,J, P,Q单独拎出来形成一棵子树。
堆排序前传——二叉树

在这里插入图片描述

堆排序前传——完全二叉树

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
堆排序用到的是顺序存储方式,链式存储方式会在后面的数据结构部分讲解。
顺序存储方式可以理解为以列表的形式存储。
在这里插入图片描述
在这里插入图片描述
如果知道孩子节点的位置为i,那么它的父节点位置为(i-1)//2。

堆排序——什么是堆

在这里插入图片描述
在这里插入图片描述

堆排序——堆的向下调整性质

在这里插入图片描述
在这里插入图片描述
以上图为例,假如数字代表游戏玩家水平,很明显2比9和7要菜,当不了王者,所以把2踢下来,从星耀中选出能力更强的9成为王者;把2放在9原来的位置发现,2比8和5还菜,也不配砖石段位,所以从砖石段位选出能力更强的8作为星耀玩家;接着,2比6和4菜,把6提上砖石,2只能排在铂金段位,至此,就完成了一次堆的向下调整,最终得到一个大根堆。
在这里插入图片描述

堆排序过程

在这里插入图片描述
第三步之所以要将最后一个元素放到堆顶再进行调整,是为了防止二叉树变为非完全二叉树。
向下调整函数的实现:

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 + 1  # j指向右孩子
        if li[j] > tmp:
            li[i] = li[j]
            i = j           # 往下看一层
            j = 2 * i + 1
        else:       # tmp更大,把tmp放到i的位置上
            li[i] = tmp     # 把tmp放到某一级领导位置上
            break
    else:
        li[i] = tmp  # 把tmp放到叶子节点上

从最后的根节点开始依次往前使用向下调整即可获得大根堆或者小根堆。
堆排序的实现:

def heap_sort(li):
    n = len(li)
    for i in range((n-2)//2, -1, -1):
        # i表示建堆的时候调整的部分的根的下标
        sift(li, i, n-1)
    # 建堆完成了
    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

这里将堆的最后一个元素放到堆顶,将原来堆顶的最大值放到空出来的最后一个位置处,不需要用额外的列表来存储堆顶数据,节省了存储空间。

堆排序的时间复杂度

向下调整函数sift()每次只走过树的左边或右边,所以时间复杂度是树的深度,即O(logn),heap_sort()函数中两个循环不是嵌套的,故时间复杂度为O(n)。
堆排序的时间复杂度是O(nlogn),和快速排序的时间复杂度一样,但在实际使用中,快速排序会比堆排序要快。

堆排序——内置模块

在这里插入图片描述

import heapq	# q->queue 优先队列
import random

li = list(range(100))
random.shuffle(li)

print(li)

heapq.heapify(li)	# 建堆,默认是小根堆

n=len(li)
for i in range(n):
	print(heapq.heappop(li), end=',')	# heappop每次弹出堆顶元素
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天涯小才

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值