Python <算法思想集结>之初窥基础算法
1. 前言
数据结构
和算法
是程序的 2
大基础结构,如果说数据
是程序的汽油,算法
则就是程序的发动机。
什么是数据结构?
指数据
在计算机中的存储方式,数据的存储方式会影响到获取数据的便利性。
现实生活中,如果把春夏秋冬的衣物全部堆放在一起,当需要某一季节的衣服时,寻找起来是困难的。
如果分门别类、有条理地存放,则寻找起来会方便很多。
同理,编写程序时,如果对程序所依赖的数据有条理、易于查找的方式进行存储,则在处理数据时,可以提升程序的整体性能。
数据结构准确说是一个空间管理概念,同样的数据使用不同的数据结构时,对程序会有空间度上的影响。
什么是算法?
理解算法,可以从 2
个角度:
- 广义角度: 算法是指处理数据时,使用的解决思路。只要能达到数据处理目的,任一解决思路都可认为是算法,也就是说程序中无处不算法。
- 狭义角度: 对各种解决问题的经验和思路进行总结、归纳,形成算法体系或算法思想。
研究算法的意义:
- 条条道路通罗马,解决同一个问题的方案往往不只一种,所以,需要在诸多的方案中选择最佳的一种,这便是研究算法的意义之一。
- 系统化算法理论,以此让算法成为一门独立的体系,当解决问题时,可以遵循问题的特征快速找到特定的算法方案。
本文主要是从狭义角度聊聊常见的几种算法。
2. 常见算法思想
2.1 穷举算法思想
穷举算法
也称为枚举算法
或暴力算法
,是一种原始算法
。
什么是原始算法
?
要了解原始算法
的概念,就需要理解计算机的思维模式。
计算机本质是一台无意识
、无经验
、无知识
积累的冰冷机器,它仅有基本的运算能力和判断能力。当然它还有一个人类无法比拟的强项,运算速度非常快。
人类思维和计算机思维的区别
比如,请找出数列[1,2,3,4,5]
中哪 2
个数字相加等于 5
。人类思维是直观性学习思维,可以直接给出答案,计算机不行。
计算机采用的是基本运算
加判断
的方案。先把1
和2
相加然后判断,如果不是,继续 把1
和 3
相加再判断……
这种思维我们称为穷举思维
或暴力思维
。
nums = [1, 2, 3, 4, 5]
for i in range(len(nums)):
for j in range(len(nums)):
if nums[i] != nums[j] and nums[i] + nums[j] == 5:
print(nums[i], nums[j])
当数列中的数字很少的时候,人类的思维有优势,当数据量很大的时候,计算机虽然采用的是笨拙的穷举思维,但是,因处理速度快,反而要远远快于人类计算出来。
本质上讲,计算机是无思维的,或者说计算机只会穷举
,所以说,算法的本质都是引导性
的。
什么是引导性?
就是你问它,它摇头或点头,你根据它的点头或摇头,继续问,它继续点头或摇头,直到得到你想到的答案。
穷法算法的思想:在一个指定的数据范围之内,通过不停地判断直到查找到正确的答案。
可以用
2
句话概括:无循环无程序,无条件无逻辑。
现根据穷举算法
的思路解决一个数学中常见的猜数字
题目。
如下图,每一个中文汉字表示一个数字,请找出每一个汉字所对应的数字。
穷举流程:
- 确定数字范围: 如果要让整个表达式有意义,则
我
和程
字所对应的数字不能是0
。所以我
和程
字的数字范围应该是1~9
之间,其它的数字范围可以是0~9
之间。 - 初始每一个汉字所对应的数字, 然后套用表达式进行计算,如果计算结果符合要求,则宣布查找到,否则,更改每一个汉字所对应的数字。
编写实现:
# 为 我很爱编程 中的每一个汉字初始数字起始边界
wo = 1
hen = 0
ai = 0
bian = 1
chen = 1
count = 0
result = 0
for wo in range(1, 10):
for hen in range(0, 10):
for ai in range(0, 10):
for bian in range(0, 10):
for chen in range(1, 10):
result = chen * pow(10, 5) + chen * pow(10, 4) + chen * pow(10, 3) + chen * pow(10, 2) + chen * pow(
10, 1) + chen
num1 = wo * pow(10, 4) + hen * pow(10, 3) + ai * pow(10, 2) + bian * pow(10, 1) + chen
count += 1
if num1 * wo == result:
print(wo, hen, ai, bian, chen)
print("次数:",count)
break
'''
输出结果
7 9 3 6 5
次数: 62429
'''
上述代码为典型的穷举算法
,代码中添加了一个计数器,用来统计最终计算多少次,仅为了观察。
穷举算法的结构有一个较大的特点,往往会出现循环语法结构层层嵌套。
在此基础上思考,是否存在优化方案,可以减少循环次数。
题目中还有一个隐式条件,我很爱编程
中的每一个汉字所对应的数字不能相同。可以把这条件加入到上述代码中。
# 为 我很爱编程 中的每一个汉字初始数字起始边界
wo = 1
hen = 0
ai = 0
bian = 1
chen = 1
count = 0
result = 0
for wo in range(1, 10):
for hen in range(0, 10):
if hen == wo:
continue
for ai in range(0, 10):
if ai == hen or ai == wo:
continue
for bian in range(0, 10):
if bian == ai or bian == hen or bian == wo:
continue
for chen in range(1, 10):
if chen == bian or chen == ai or chen == hen or chen == wo:
continue
result = chen * pow(10, 5) + chen * pow(10, 4) + chen * pow(10, 3) + chen * pow(10, 2) + chen * pow(
10, 1) + chen
num1 = wo * pow(10, 4) + hen * pow(10, 3) + ai * pow(10, 2) + bian * pow(10, 1) + chen
count += 1
if num1 * wo == result:
print(wo, hen, ai, bian, chen)
print("次数:", count)
break
'''
输出结果:
7 9 3 6 5
次数: 18666
'''
从 2
次代码的运行结果可知,循环次数减少了 62429-18666=43763
次。虽然减少了循环次数,因没有从本质上改变算法结构,所以说还是最原始的穷举算法
思路。
2.2 递推算法思想
计算机的思维本质是穷举。但是,人的思维是知识性、探索性思维,可以在解决问题时,发现问题中的规律,并通过计算机语言告诉计算机,这样可以在计算时绕过一些不必要的计算。
研究算法的本质就是通过发现数据间的规律、减少穷举的次数。
什么是递推算法?
简单讲,不断地利用已知信息推导出最终结果。显然,已知信息和最终结果数据之间一定要存在某些内在联系或规律。
递推算法又分为顺推法
和逆推法
。
顺推法: 从已知条件出发,逐步推算出所需要的结果。
逆推法: 从已知的结果出发,用迭代式逐步推算出问题开始,逆推法的本质就是逆向思维。
这里通过 2
个案例分别介绍顺推法
和逆推法
。
斐波拉契数列
数列 [1,1,2,3,5,8,13,21,34,55,……]
就是一个符合斐波拉契
关系的数列。
斐波拉契数列特点:
- 数列中的第
1
、2
位置的数字是1
,这是已知信息。 - 从第
3
个位置开始,其值为前面2
个数字相加的结果。 知道了第3
个位置的值,也将知道第4
个位值,依此类推,可以求解出任何位置的数字。
num1 = 1
num2 = 1
for i in range(12):
# 推导出第 3 个位置的数字
num3 = num1 + num2
print(num3, end="\t")
# 为计算后续数据做准备
num1 = num2
num2 = num3
求解斐波拉契
数列的方案就是典型的顺推思维
。
如有数列[1,4,10,22,46……]
,分析数列中前面几个数字的规律,输出数列的前 20
个数字。分析后可发现第 1
个位置的数字 1
是已知信息,从第 2
个位置开始,其值和前一个值符合2*x+2
的线性规律,所以也可以递推算法求解。
猴子吃桃
有一只猴子,第 1
天摘了若干桃子,吃了桃子的一半后又吃了一个,第 2
天也是吃了桃子一半后再吃了一个……如此类推,到第 10
天时,还剩余 1
只桃子。请问猴子第 1
天摘了多少只桃子。
分析:
第
10
天还剩余1
只桃子,可以看成是已知信息,已知信息属于结果信息。求解第
1
天的桃子总量,需求解的是开始问题。
可以使用逆推算法
,也就是我们经常讲的逆向思维解决猴子吃桃问题。
找出数据之间的关系:
- 第
10
有1
个桃子,第10
天的这1
个桃子是取第9
天桃子的一半减一个剩余的。 - 前一天的桃子除以
2
减1
等于后1
天的桃子,或者说,前1
天的桃子等于(后1
天的桃子加1
)乘以2
。
有了数列之间的关系,编码就很容易了。
# 第 10 天的桃子数,也是已知条件
num = 1
for i in range(9):
# 向第 1 天推进
num = (num + 1) * 2
print(num)
类似的问题还有很多:
如猜年龄问题。
有 5
个小孩子,问第 1
个小孩子的年龄是多少?他说他是第 2
个小孩子的年龄加 2
岁。
问第 2
个小孩子的年龄时,他说他是第3
个小孩子的年龄加2
岁。
问第3
个小孩子的年龄时,他说他是第 4
个小孩子年龄加2
岁。
问第 4
个小孩子年龄时,他说他是第 5
个小孩子的年龄加2
岁。
问第 5
个小孩子的年龄时,他说他的年龄是 6
岁,求解每一个小孩子的年龄。
这个问题也是典型的逆推问题。第 5
个小孩子的年龄是已知的,而且知道与前一个小孩子年龄的关系前一个小孩子的年龄=后一个小孩子年龄+2。满足数学上的线性函数关系。
一层层回推就能计算出第 1
个小孩子的年龄是:14
岁。
age = 6
for i in range(4, 0, -1):
age = age + 2
print("第{0}个小孩子的年龄是{1}".format(i,age))
'''
输出结果
第4个小孩子的年龄是8
第3个小孩子的年龄是10
第2个小孩子的年龄是12
第1个小孩子的年龄是14
'''
上述问题虽然简单,但能精确地描述递推算法的思想。
2.3 递归算法
具体解决问题时,总是一种算法借鉴另一种算法,或一个算法中融入另一种算法。算法之间互相交织、迭代而诞生出新的算法。
前文所说,穷举
才是计算机的本质,其它算法无非是通过分析问题、发现规律、减少算法的实施过程中的次数。
什么是递归算法?
通过不停调用函数
本身从而达到解决问题的目地。
如现实生活中经常会遇到的问题,我要找到小王的电话号码,可以帮助理解递归过程。
想知道朋友小王
的电话号码,先找到朋友小李
,小李
说他也没有,但是他会帮问问小张
,小张
说他也没有,他会问问小胡
,小胡说他知道。
这里面包括 2
条线。
- 递进线:
我
-(问)->小李
-(问)->小张
-(问)->小胡
(结果)。 - 回溯线: 小胡-(结果)->小张-(结果)->小李-(结果)->我。
递归算法的特点:
- 通过
递进线
寻求帮助。递推线的最终必须有能得到帮助的时候(如最后小胡知道小王的电话号码),否则会成为死结。表现在编码实施过程中需要有调用终止的时候。 - 通过
回溯线
求解出原始问题。
前面的斐波拉契数列
也可以使用递归算法解决。比如说,想知道在第 12
位置的数字是多少。
- 递进线:求数列第
12
位置的值,求助于第11
位置的值,然后再求助于第10
的值, 一至求助到第1
,2
位置。 - 回溯线:通过回溯求解出原始问题。
def fb(pos):
if pos == 1 or pos == 2:
# 求助的终点
return 1
# 求助
return fb(pos - 1) + fb(pos - 2)
res = fb(12)
print(res)
求解年龄的问题也可以使用递归算法实现。
def get_age(number):
if number == 5:
return 6
return get_age(number + 1) + 2
print(get_age(1))
递归算法可以用于 2
类问题的求解:
替代循环语法结构。一个函数就是一个逻辑实现的封装,反复调用自己,则可认为重复执行相同逻辑。
递归比循环的性能低下。能使用循坏解决的问题就不要使用递归。
一个看似复杂的问题,其实最终答案归结到一个小问题上,如求阶乘、斐波拉契之类的问题。
递归更多应用于此类型问题的求解。
斐波拉契
和求年龄
的问题即可以使用前文的递推算法思想
实现,也可以使用递归
算法实现。说明:
解决一个问题不能拘泥一种方案。
算法与算法之间会有重叠、借鉴、融合之处。
任何语言都提供有递归调用方式。可以利用递归的特点对数据进行处理。
递归的底层依赖于栈数据结构,递归的具体细节本文不做太多讲解,本文意在概括常见算法。
在算法理论中,回溯本身就是一种算法方案,可独立解决很多实际问题。
回溯法是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,但可以利用回溯的技术求解。回溯法是搜索算法中的一种控制策略。
它的基本思想是:从问题的某一种状态(一般是默认的初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候,先退几步,接着从另一种可能的“状态”出发,继续搜索,直到所有的“路径”都尝试过。
回溯思路在我们在现实生活中无处不在,对此体现的较具体的就是下棋,还有一个典型的应用就是走迷宫。
因回溯已经内置在递归算法中,一般需要使用回溯解决的问题,都会用到递归。
2.4 分治思想
将一个计算复杂的问题分为若干子问题来进行求解,然后合并各个小问题得到原始问题的最终求解。
分治算法的特点:
- 原始问题和分解出来的子问题的问题形式相同,只是数据规模不相同,也就是说无论是原始问题,还是分解后的子问题,都在解决同一个问题。
- 分解出来的子问题具有完全独立性,子问题是原始问题的缩影。
分治算法实时有 2
个过程:
- 分治: 原始问题能分解成若干较小的相同问题,子问题因规模较小,更容易求解到答案。
- 合并: 合并子问题得到原始问题的求解。
因子问题与原始问题相同,一般会使用递归算法多次迭代。
如二分查找
以及快速排序
,都是分治的思想的应用。
二分查找
的具体实现过程,请查阅我的博文:https://blog.51cto.com/gkcode/5250956
快速排序
的具体的实现过程,请查阅我的博文:https://blog.51cto.com/gkcode/5195936
2.5 贪心算法思想
贪心算法总是做出在当前看来最好的选择,并从不整体最优考虑,只是局部最优秀。虽然贪心算法总是从局部最优求解,但有时也有可能让最终求解也是最优的。
贪心算法的特点:
不能保证最后的解是最优的。
不能用来求最大或最小解问题。
只能求满足某些约束条件的可行解。
贪婪算法最典型的案例:找零钱。
问题描述:在超市购物时,收银员找零钱时,如何使找回零钱的纸币数最少。
贪心算法的思路是从最大面值的币种开始,按递减的顺序考虑各种币种。也就是先尽量用大面值的币种,当不足大面值币种的金额时才去考虑下一种较小面值的币种。
这里的贪心表现在最大可能使用最大金额的币种。显然,贪心算法在此问题上是可行的。
编码实现:
假设人民币有 100
、50
、20
、10
、5
、2
、1
、0.5
、0.1
几种面额,且数量无限,找零钱时,请尽量实现找给顾客的零钱所用到的币种的总数量最少。
# 币种
bi_zhongs = [100, 50, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1]
# 记录最终结果
dic = {}
# 找霍
lq = 68.9
# 为了便于计算,扩大100倍
lq = lq * 100
for i in range(len(bi_zhongs)):
if lq >= bi_zhongs[i] * 100:
# 计算张数
count = lq // (bi_zhongs[i] * 100)
dic[bi_zhongs[i]] = int(count)
# 余额
lq = lq % (bi_zhongs[i] * 100)
if lq == 0:
break
# 输出每一种币种的数量
print(dic)
'''
输出结果:
{50: 1, 10: 1, 5: 1, 2: 1, 1: 1, 0.5: 1, 0.2: 2}
'''
3. 总结
本文介绍了常见的几种基础算法 ,除些之外,还有更多算法思想。如动态规划、摸拟思想……限于篇幅原因,本文中即不一一罗列,也不深研算法内在细节。
后续会为某些经典算法单独开文,详细介绍其算法的微妙之处。
万变不离其宗,研究算法时即要做到能对各种算法独立分析,又要做到融合贯通。
Python <算法思想集结>之初窥基础算法的更多相关文章
- Python <算法思想集结>之抽丝剥茧聊动态规划
1. 概述 动态规划算法应用非常之广泛. 对于算法学习者而言,不跨过动态规划这道门,不算真正了解算法. 初接触动态规划者,理解其思想精髓会存在一定的难度,本文将通过一个案例,抽丝剥茧般和大家聊聊动态规 ...
- 基本算法思想Java实现的详细代码
基本算法思想Java实现的详细代码 算法是一个程序的灵魂,一个好的算法往往可以化繁为简,高效的求解问题.在程序设计中算法是独立于语言的,无论使用哪一种语言都可以使用这些算法,本文笔者将以Java语言为 ...
- Python学习之路——迭代器、生成器、算法基础、正则
一.迭代器: 迭代器是访问集合元素的一种方式. 迭代器对象是从集合的第一个元素开始访问,直到所有的元素被访问完结束. 迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退. 另外,迭代 ...
- kmeans算法思想及其python实现
第十章 利用k-均值聚类算法对未标注的数据进行分组 一.导语 聚类算法可以看做是一种无监督的分类方法,之所以这么说的原因是它和分类方法的结果相同,区别它的类别没有预先的定义.簇识别是聚类算法中经常使用 ...
- js算法初窥05(算法模式02-动态规划与贪心算法)
在前面的文章中(js算法初窥02(排序算法02-归并.快速以及堆排)我们学习了如何用分治法来实现归并排序,那么动态规划跟分治法有点类似,但是分治法是把问题分解成互相独立的子问题,最后组合它们的结果,而 ...
- python爬虫 scrapy2_初窥Scrapy
sklearn实战-乳腺癌细胞数据挖掘 https://study.163.com/course/introduction.htm?courseId=1005269003&utm_campai ...
- 初窥图像识别与k-means算法
前段时间做了一个车型识别的小项目,思路是利用k-means算法以及词袋模型来做的. 近年来图像识别的方法非常非常多,这边只记录一下我那个项目的思路,核心思想是k-means算法和词汇树. 很遗憾没有做 ...
- python基础算法
一.简介 定义和特征 定义:算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制.也就是说,能够对一定规范的输入,在有限时 ...
- js算法初窥07(算法复杂度)
算法复杂度是我们来衡量一个算法执行效率的一个度量标准,算法复杂度通常主要有时间复杂度和空间复杂度两种.时间复杂度就是指算法代码在运行最终得到我们想要的结果时所消耗的时间,而空间复杂度则是指算法中用来存 ...
随机推荐
- APICloud首款全功能集成开发工具重磅发布,彰显云端一体理念
近日,APICloud重磅推出首款云端一体的全功能集成开发工具--APICloud Studio 2.为了更深入了解这款开发工具的特性及优势,APICloud CTO 邹达针对几个核心问题做出了解答. ...
- 通读Python官方文档之wsgiref(未完成)
wsgirf-WSGI功能及参考实现 源码:Lib/wsgiref Web服务器网关接口(Web Server Gateway Interface, WSGI),是用Python写的一个服务器软件和w ...
- 前端网络安全——Cookies
一.Cookies特性 1.前端数据存储 2.后端通过http头设置 3.请求时通过http头传给后端 4.前端可读写 5.遵守同源策略 二.Cookies内容 1.域名 2.有效期,删除cookie ...
- CCF201909-1小明种苹果
解题思路:定义一个二维数组来存放输入的信息,第一列用来存放所有果树的初始值,然后遍历数组.具体思路见代码注释. 第一遍提交得了80分,看了半天才明白了原因,快被自己蠢死...... 定义数组应该为a[ ...
- js知识梳理6:关于函数的要点梳理(2)(作用域链和闭包)
写在前面 注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者.有发现什么问题的,欢迎留言指出 ...
- Java报错:Unable to find setter method for attribute: [x]
在学习JavaWeb JSTL与自定义标签时遇到的坑,用的老师给的代码结果直接原地报错:javax.servlet.ServletException: org.apache.jasper.Jasper ...
- python---冒泡排序的实现
冒泡排序 思想 列表中有n个数, 每两个相邻的数, 如果前边的数比后边的数大, 就交换. 关键点: 趟: 总共执行 n-1趟 无序区: 第 i 趟时, 索引 0~ n-1-i 为无序区 ...
- 查域名对应ip,测试端口是否可访问通
根据命令查询软件包名称 yum provides */nslookup 根据域名解析ip nslookup 域名 示例:nslookup smtp.163.com 测试端口 telnet ip 端口 ...
- Educational Codeforces Round 119 (Div. 2), (C) BA-String硬着头皮做, 能做出来的
题目链接 Problem - C - Codeforces 题目 Example input 3 2 4 3 a* 4 1 3 a**a 6 3 20 **a*** output abb abba b ...
- Codeforces Round #710 (Div. 3) Editorial 1506A - Strange Table
题目链接 https://codeforces.com/contest/1506/problem/A 原题 1506A - Strange Table Example input 5 1 1 1 2 ...