Python数据分析基础之CSV文件(1)

参考资料:
《Python数据分析基础》,作者[美]Clinton W. Brownley,译者陈光欣,中国工信出版集团,人民邮电出版社

CSV文件简述

  CSV(comma-separated value,逗号分隔值)文件格式是一种非常简单的数据存储与分享方式。CSV文件将数据表格存储为纯文本,表格(或电子表格)中的每个单元格都是一个数值或字符串。与Excel文件相比,CSV文件的一个主要优点是有很多程序可以存储、转换和处理纯文本文件;相比之下,能够处理Excel文件的程序却不多。
  当我们使用CSV文件时,确实会失去某些Excel功能:在Excel电子表格中,每个单元格都有一个定义好的“类型”,CSV文件中的单元格则只是原始数据。使用CSV文件的另一个问题是它只能保存数据而不能保存公式

CSV文件初体验

  我们打开一个电子表格,向其中加入数据,如下图所示。
在这里插入图片描述  把文件保存在合适的位置。我们用记事本来打开这个文件,如下图所示。
在这里插入图片描述  可以看到,supplier_data.csv确实是纯文本文件。

用Python读写CSV文件

1.基础Python,不使用csv模块

  我们新建一个Python脚本,命名为1csv_read_with_simple_parsing_and_write.py。输入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3

import sys

input_file = sys.argv[1]
output_file = sys.argv[2]

with open(input_file, 'r', newline='') as filereader:
with open(output_file, 'w', newline='') as filewriter:
header = filereader.readline()
header = header.strip()
header_list = header.split(',')
print(header_list)
filewriter.write(','.join(map(str, header_list)) + '\n')
for row in filereader:
row = row.strip()
row_list = row.split(',')
print(row_list)
filewriter.write(','.join(map(str, row_list)) + '\n')

  下面我们来解释一下上面的代码。

1
#!/usr/bin/env python3

  这一行称为shebang行。在Python脚本中,我们应该一直把它作为第一行。以#开头的行为单行注释,在Windows系统中,计算机不读取也不执行这一行代码。但是,安装了Unix系统的计算机使用这一行来找到执行文件中代码的Python版本。加入这一行可以使脚本在不同操作系统的计算机之间具有可移植性。

1
2
input_file = sys.argv[1]
output_file = sys.argv[2]

  这两行代码使用sys模块的argv参数,它是一个传递给Python脚本的命令行参数列表,也就是当你运行脚本时在命令行输入的内容。在argv这个特殊的列表中,第一个元素argv[0]用做脚本名称,argv[1]则用作CSV输入文件的路径和文件名,argv[2]用作CSV输出文件的路径和文件名。在上面的代码中,argv[1]argv[2]被分别赋值给变量input_fileoutput_file

1
2
with open(input_file, 'r', newline='') as filereader:
with open(output_file, 'w', newline='') as filewriter:

  这里的with语句可以在语句结束时自动关闭文件对象。

1
2
3
header = filereader.readline()
header = header.strip()
header_list = header.split(',')

  这里使用了文件对象的readline方法读取输入文件中的第一行数据。第一行是标题行,读入后将其作为字符串赋给变量headerstrip()去掉了header字符串两端的空格、制表符和换行符。接下来使用split(',')将字符串用逗号拆分成列表并赋给变量header_list,列表中的每个值都是一个列标题。

1
filewriter.write(','.join(map(str, header_list)) + '\n')

  这行代码使用filewriter对象的write()方法将header_list中的每个值写入输出文件。map()函数将str()函数应用于header_list中的每个元素,确保每个元素都是字符串。join()函数在header_list中的每个值之间插入一个逗号,将这个列表转换为一个字符串。在此之后,在这个字符串最后添加一个换行符\nfilewriter对象将这个字符串写入输出文件,作为输出文件的第一行。

1
2
3
4
5
for row in filereader:
row = row.strip()
row_list = row.split(',')
print(row_list)
filewriter.write(','.join(map(str, row_list)) + '\n')

  这里创建了一个for循环,在输入文件剩余的各行中迭代。strip()除去每行字符串两端的空格、换行符和制表符。split(',')用逗号将字符串拆分成一个列表并赋给变量row_list,列表中的每个值都是这行中某一列的值。最后,将这些值打印到屏幕上并写入输出文件。
  我们在命令行窗口运行这个脚本,如下图所示。
在这里插入图片描述  输入文件的所有行都被打印到了屏幕上,也被写入了输出文件。
在这里插入图片描述

2.使用pandas模块

  使用pandas模块处理CSV文件的代码如下所示:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env python3

import sys
import pandas as pd

input_file = sys.argv[1]
output_file = sys.argv[2]
data_frame = pd.read_csv(input_file)
print(data_frame)
data_frame.to_csv(output_file, index=False)

  这里我们创建了一个变量data_frame,这种数据方式叫做数据框。数据框中保留了“表格”这种组织方式,不需要使用列表套列表的方式来分析数据。数据框包含在pandas中,如果我们没有在脚本中导入pandas模块,就不能使用数据框。
  在学习阶段,将这个变量命名为data_frame是可以的,但是以后,我们应该使用更有描述性的变量名。
  我们在命令行窗口运行这个脚本,如下图所示。
在这里插入图片描述)在这里插入图片描述

Python与正则表达式

参考资料:
1.菜鸟教程-Python 3 正则表达式,网址:https://www.runoob.com/python3/python3-reg-expressions.html
2.《Python从小白到大牛》,作者关东升,清华大学出版社
3.《Python数据分析基础》,作者[美]Clinton W. Brownley,译者陈光欣,中国工信出版集团,人民邮电出版社

简述正则表达式

  正则表达式(Regular Expression)是一个特殊的字符序列,它能帮助你方便的检查一个字符串是否与某种模式匹配。在Python中,正则表达式的应用非常广泛,如数据挖掘、数据分析、网络爬虫、输入有效性验证等。
  re模块使Python拥有全部的正则表达式功能。我们使用下面的语句来导入该模块:

1
import re

正则表达式字符串

  正则表达式字符串由普通字符和元字符组成。
  普通字符是按照字符字面意义表示的字符。元字符是预先定义好的一些特殊字符,如\w+\.都属于元字符。
  下面是一些基本元字符:
字符|说明
——|——
\|转义符号
.|表示任意一个字符
+|表示重复一次或多次
*|表示重复零次或多次
?|表示重复零次或一次
||选择符号,表示“或”
{}|定义量词
[]|定义字符类
()|定义分组
^|表示取反,或匹配一行的开始
$|匹配一行的结束
  上面提到了元字符^$,它们可以匹配一行字符串的开始和结束。当以^开始时,要求一行字符串的开始位置匹配;当以$位置结束时,要求一行字符串的结束位置匹配。需要注意的是,正则表达式\w+@qq\.com^\w+@qq\.com$是不同的。
  定义字符类需要使用元字符[]。例如说我们想在输入的字符串中匹配Python或python,则可以使用正则表达式[Pp]ython
  在正则表达式中指定不想出现的字符,可以在字符类之前加^符号进行字符类取反。比如说,正则表达式[^0123456789]表示输入的字符串中出现非0~9数字即匹配,也就是出现在[0123456789]之外的任意字符即匹配。
  上面所说的正则表达式[^0123456789]写起来非常麻烦。事实上,我们引入区间的概念之后,这种连续的字符可以使用区间来表示。区间使用连字符-表示。例如[0123456789]可以采用区间表示为[0-9][^0123456789]可以采用区间表示为[^0-9]。区间也可以表示连续的英文字母字符类,例如[a-z]表示所有小写字符类,[A-Z]表示所有大写字母字符类。除此之外,也可以表示多个不同区间,例如[A-Za-z0-9]表示所有字母和数字字符类,[0-36-8]表示0、1、2、3、6、7、8几个数字字符组成的字符类。
  正则表达式提供了预定义字符类。预定义字符类如下表所示:
字符|说明
——|——
.|匹配任意一个字符
\\|匹配反斜杠\字符
\n|匹配换行符
\r|匹配回车符
\f|匹配一个换页符
\t|匹配一个水平制表符
\v|匹配一个垂直制表符
\s|匹配一个空格符(等价于[\t\n\r\f\v])
\S|匹配一个非空格符(等价于[^\s])
\d|匹配一个数字字符(等价于[0-9])
\D|匹配一个非数字字符(等价于[^0-9])
\w|匹配任何语言的单词字符(包括英文字母、亚洲文字等)、数字和下划线等字符,如果正则表达式编译标志设置为ASCII,则只匹配[a-zA-Z0-9_]
\W|等价于[^\w]

量词

  在正则表达式中,元字符只能匹配显示一次字符或字符串。如果想要匹配显示多次字符或字符串,可以使用量词。
  正则表达式中的量词如下表所示:
字符|说明
——|——
?|出现零次或一次
*|出现零次或多次
+|出现一次或多次
{n}|出现n次
{n,m}|至少出现n次但不超过m次
{n,}|至少出现n次
  下面是一个量词的使用示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import re

m = re.search(r'\d?', '87654321') # 出现数字一次
print(m) # 匹配字符'8'

m = re.search(r'\d?', 'ABC') # 出现数字零次
print(m) # 匹配字符''

m = re.search(r'\d*', '87654321') # 出现数字多次
print(m) # 匹配字符'87654321'

m = re.search(r'\d*', 'ABC') # 出现数字零次
print(m) # 匹配字符''

m = re.search(r'\d+', '87654321') # 出现数字多次
print(m) # 匹配字符'87654321'

m = re.search(r'\d+', 'ABC')
print(m) # 不匹配

m = re.search(r'\d{8}', '87654321') # 出现数字8次
print(m) # 匹配字符'87654321'

m = re.search(r'\d{8}', 'ABC')
print(m) # 不匹配

m = re.search(r'\d{7,8}', '87654321') # 出现数字8次
print(m) # 匹配字符'87654321'

m = re.search(r'\d{9,}', '87654321')
print(m) # 不匹配

  输出结果如下:

1
2
3
4
5
6
7
8
9
10
<re.Match object; span=(0, 1), match='8'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 0), match=''>
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None
<re.Match object; span=(0, 8), match='87654321'>
None

  量词还可以细分为贪婪量词和懒惰量词。贪婪量词会尽可能多地匹配字符,懒惰量词会尽可能少地匹配字符。大多数计算机语言的正则表达式量词默认是贪婪量词,如果要使用懒惰量词,在量词之后加?即可。
  下面是一个示例代码:

1
2
3
4
5
6
7
import re

m = re.search(r'\d{5,8}', '87654321') # 出现数字8次
print(m) # 匹配字符'87654321'

m = re.search(r'\d{5,8}?', '87654321') # 出现数字5次
print(m) # 匹配字符'87654'

  输出结果如下:

1
2
<re.Match object; span=(0, 8), match='87654321'>
<re.Match object; span=(0, 5), match='87654'>

分组

  量词只能重复显示一个字符,如果想让一个字符串作为整体使用量词,可以将这个字符串放到()中,这就是分组,也被称作子表达式。
  下面是一个使用分组的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
import re

p = r'(121){2}'
m = re.search(p, '121121abcabc')
print(m) # 匹配
print(m.group()) # 返回匹配字符串
print(m.group(1)) # 获得第一组内容
p = r'(\d{3,4})-(\d{7,8})'
m = re.search(p, '010-87654321')
print(m) # 匹配
print(m.group()) # 返回匹配字符串
print(m.groups()) # 获得所有组内容

  输出结果如下:

1
2
3
4
5
6
<re.Match object; span=(0, 6), match='121121'>
121121
121
<re.Match object; span=(0, 12), match='010-87654321'>
010-87654321
('010', '87654321')

  在Python中访问分组时,除了可以通过组编号访问,还可以通过组名访问。这需要我们在正则表达式中为组命名。组命名通过在组开头添加?P<分组名>实现。
  在正则表达式中,反向引用语法是\组编号。组编号从1开始。
  前面所说的分组称为捕获分组。捕获分组的子表达式结果被暂时保存到内存中,以备表达式或其他程序通过组编号或组名进行引用。但是有时,我们并不想引用子表达式的匹配结果,不想捕获匹配结果,只是将小括号作为一个整体进行匹配。这个时候我们就可以使用非捕获分组。在组开头使用?:可以实现非捕获分组。

Python的re模块

re.search()函数

  re.search()函数在输入字符串中查找,返回第一个匹配内容,如果找到一个则match对象,如果没有找到则返回None。
  函数语法如下:

1
re.search(pattern, string, flags=0)

  参数说明如下:
参数|参数说明
——|——
pattern|匹配的正则表达式
string|要匹配的字符串
flags|标志位,用于控制正则表达式的匹配方式,如是否区分大小写,多行匹配等

re.match()函数

  re.match()函数在输入字符串开始处查找匹配内容,如果找到一个则match对象,如果没有找到则返回None。
  函数语法如下:

1
re.match(pattern, string, flags=0)

  参数说明如下:
参数|参数说明
——|——
pattern|匹配的正则表达式
string|要匹配的字符串
flags|标志位,用于控制正则表达式的匹配方式,如是否区分大小写,多行匹配等
  下面我们简单讨论一下re.search()和re.match()的区别。
  re.match()只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;re.search()匹配整个字符串,直到找到一个匹配。
  re.search()和re.match()如果匹配成功都返回match对象。下面是match对象的一些常用方法:
方法|返回值
——|——
group()|返回匹配的子字符串
start()|返回子字符串的开始索引
end()|返回子字符串的结束索引
span()|返回子字符串的跨度,它是一个二元素的元组

re.findall()函数

  re.findall()函数在输入字符串中查找所有匹配内容,如果匹配成功,则返回match列表对象,如果匹配失败则返回None。
  函数语法如下:

1
re.findall(string[, pos[, endpos]])

  参数说明如下:
参数|参数说明
——|——
string|待匹配的字符串
pos|可选参数,指定字符串的起始位置,默认为0
endpos|可选参数,指定字符串的结束为止,默认为字符串的长度

re.finditer()函数

  re.finditer()函数在输入字符串中查找所有匹配内容,如果匹配成功,则返回容纳match的可迭代对象,通过迭代对象每次可以返回一个match对象,如果匹配失败则返回None。
  函数语法如下:

1
re.finditer(pattern, string, flags=0)

  参数说明如下:
参数|参数说明
——|——
pattern|匹配的正则表达式
string|要匹配的字符串
flags|标志位,用于控制正则表达式的匹配方式,如是否区分大小写,多行匹配等

re.split()函数

  re.split()函数按照匹配的字符串进行字符串分割,返回字符串列表对象。
  函数语法如下:

1
re.split(pattern, string[, maxsplit=0, flags=0])

  参数说明如下:
参数|参数说明
——|——
pattern|匹配的正则表达式
string|要匹配的字符串
maxsplit|分割次数,默认为0,不限制次数
flags|标志位,用于控制正则表达式的匹配方式,如是否区分大小写,多行匹配等

re.sub()函数

  re.sub()函数用于替换匹配的子字符串,返回替换之后的字符串。
  函数语法如下:

1
re.sub(pattern, repl, string, count=0, flags=0)

  参数说明如下:
参数|参数说明
——|——
patern|正则中的模式字符串
repl|替换的字符串,也可为一个函数
string|要被查找替换的原始字符串
count|模式匹配后替换的最大次数,默认为0,表示替换所有的匹配
flags|编译时用的匹配模式,数字形式

re.compile()函数

  re.compile()函数可以编译正则表达式。
  函数语法如下:

1
re.compile(pattern[, flags=0])

  参数说明如下:
参数|参数说明
——|——
pattern|一个字符串形式的正则表达式
flags|可选参数,表示匹配模式,如是否区分大小写,多行匹配等
  re.compile()函数返回一个编译的正则表达式对象regex。

正则表达式修饰符

修饰符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别匹配
re.M 设置多行模式,多行匹配,影响^和$
re.S 使.匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符,影响\w, \W, \b, \B
re.X 设置详细模式,详细模式下可以在正则表达式中添加注释, 可以有空格和换行

【算法积累】最长公共前缀

题目

  注:本题来自LeetCode题库14.最长公共前缀。

编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。

  示例1:

1
2
输入: ["flower","flow","flight"]
输出: "fl"

  示例2:

1
2
3
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。

解法1:字符串排序取首尾公共前缀

  简单来说,把字符串按字母顺序排序后,首字符串和尾字符串的公共前缀,就是这一组字符串的公共前缀。这种解法用C语言并不是很好实现。因为Python中内置函数min()和max(),在这里我使用Python 3来解决。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 语言:Python 3
# 执行用时:36ms
# 内存消耗:12.8MB
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
if not strs:
return ""
strMin = min(strs)
strMax = max(strs)
string = []
for i in range(0, len(strMin)):
if strMin[i] == strMax[i]:
string.append(strMin[i])
else:
break
return "".join(string)

解法2:取最短字符串逐个比对

  上面的解法依赖于Python的内置函数min()和max()。我们换一种思路,取出其中最短的字符串,用它和其他的字符串一一比对,找出公共前缀。
  特殊地,如果输入的字符串数组长度为0,则只需返回空字符串“”;如果输入的字符串数组长度为1,则只需返回这个字符串,即strs[0]。
  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 语言:C
// 执行用时:0ms
// 内存消耗:7.1MB
char * longestCommonPrefix(char ** strs, int strsSize){
int strLen = 0;
char *shortStr = NULL;
int shortLen = 0;
int i, j;
if (strs == NULL || strsSize == 0)
return "";
if (strs != NULL && strsSize == 1)
return strs[0];
shortStr = strs[0];
shortLen = strlen(strs[0]);
for (i = 0; i < strsSize; i++) {
strLen = strlen(strs[i]);
if (shortLen > strLen) {
shortLen = strLen;
shortStr = strs[i];
}
}
for (j = 0; j < shortLen; j++) {
for (i = 0; i < strsSize; i++) {
if (strs[i][j] != shortStr[j]) {
if (j == 0) {
return "";
} else {
shortStr[j] = '\0';
return shortStr;
}
}
}
}
return shortStr;
}

解法3:水平扫描法

  为了便于理解,我截取了LeetCode官方题解中水平扫描法的图解:
Alt
  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
语言:C
执行用时:4ms
内存消耗:7.1MB
char * longestCommonPrefix(char ** strs, int strsSize){
int i, j;
if (strsSize == 0)
return "";
if (strsSize == 1)
return strs[0];
for (i = 0; i < strlen(strs[0]); i++) {
for (j = 1; j < strsSize; j++)
if (strs[0][i] != strs[j][i]) {
strs[0][i] = '\0';
}
}
return strs[0];
}

解法4:使用Python的os.path模块

  最后放一个“大招”吧。正所谓“人生苦短,我用Python”,就在Python的os.path模块中,有一个叫做commonprefix()的方法,可以说是为这道题而生。
  我们来对os.path.commonprefix()做一下说明。它可以返回list(多个路径)中所有path共有的最长的路径。怎么样,是不是完美适配这道题呢?
  还是放一下代码吧,虽然只有短短一行:

1
2
3
4
5
6
# 语言:Python 3
# 执行用时:28ms
# 内存消耗:12.7MB
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
return os.path.commonprefix(strs)

Python编码规范总结

前言

  有句俗话说得好:“没有规矩,不成方圆。”这句话在计算机编程领域同样适用。大家都知道,每个人编写代码的习惯与风格是不一样的。而代码风格的统一可以带来很多好处,我认为从最直观的角度来说就是增强了代码的可读性。此外,统一代码风格还有更容易发现bug、略微提高性能等好处。
  之前曾经读过一篇文章,里面提到这样一句话:“任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。”这句话对我的影响很深。我是一名大一本科在读学生,在平时的学习中不乏和同学互相讨论研究代码。我说过最多的一句话大概就是:“你这个代码写得太不规范了。”但是从来没有任何一个同学重视这句话,他们甚至会不屑一顾地回复我:“无所谓,又不是不能运行。”
  但我认为,在系统学习一门编程语言之前,首先应该学习编码规范,从开始就养成一个良好的习惯,对于以后的学习来说也是有百利而无一害。只有这样,我们才可以编写出可读性高,无论自己和他人都易于理解的代码。

Python编码规范

参考书籍:《Python从小白到大牛》
作者:关东升
出版社:清华大学出版社
ISBN:978-7-302-50933-2
除此之外,我还参考了PEP 8编码规范。

1.命名规范

  1.包名:全部小写字母,中间可以由点分隔开,不推荐使用下划线。作为命名空间,包名应该具有唯一性,推荐采用公司或组织域名的倒置。
  2.模块名:全部小写字母,如果由多个单词构成,可以用下划线隔开。
  3.类名:采用大驼峰法(即每一个单词的首字母都大写)命名。
  4.异常名:异常属于类,命名同类命名,但应该使用Error作为后缀。
  5.变量名:全部小写字母,如果由多个单词构成,可以用下划线隔开。如果变量应用于模块或函数内部,则变量名可以由单下划线开头;变量类内部私有使用变量名可以双下划线开头。不要命名双下划线开头和结尾的变量,这是Python保留的。另外,避免使用小写L、大写O和大写I作为变量名。
  6.函数名和方法名:命名同变量命名。
  7.常量名:全部大写字母,如果由多个单词构成,可以用下划线隔开。

2.注释规范

  Python中的注释语法有3种,分别是单行注释、多行注释和文档注释。

2.1 文件注释

  文件注释就是在每一个文件开头添加注释,采用多行注释。文件注释通常包括如下信息:版权信息、文件名、所在模块、作者信息、历史版本信息、文件内容和作用等。
  文件注释要根据实际情况包括内容。

2.2 文档注释

  文档注释就是文档字符串,注释内容能够生成API帮助文档,可以使用Python官方提供的pydoc工具从Python源代码文件中提取这些信息,也可以生成HTML文件。所有公有的模块、函数、类和方法都应该进行文档注释。
  文档注释推荐使用一对三重双引号“””””包裹起来,注意不推荐使用三重单引号“’’’”。文档注释应该位于被注释的模块、函数、类和方法内部的第一条语句。如果文档注释一行能够注释完成,结束的三重双引号也在同一行。如果文档注释很长,第一行注释之后要留一个空行,然后剩下的注释内容换行要与开始三重双引号对齐,最后结束的三重双引号要独占一行,并与开始三重双引号对齐。

2.3 代码注释

  程序代码中处理文档注释时还需要在一些关键的地方添加代码注释,文档注释一般是给一些看不到源代码的人看的帮助文档,而代码注释是给阅读源代码的人参考的。代码注释一般采用单行注释和多行注释。
  单行注释和多行注释要求与其后的代码具有一样的缩进级别。尾端进行注释要求注释内容极短,应该再有足够的空白(至少两个空格)来分开代码和注释。

2.4 使用TODO注释

  很多IDE工具都为源代码提供了一些特殊的注释,就是在代码中加一些标识,便于IDE工具快速定位代码,TODO注释就是其中的一种。
  TODO注释虽然不是Python官方所提供的,但是主流的IDE工具也都支持TODO注释。有TODO注释说明此处有待处理的任务,或代码没有编写完成。
  以PyCharm为例,TODO注释可以在PyCharm的TODO视图查看,单击其中的TODO可跳转到注释处。

3.导入规范

  导入语句总是放在文件顶部,位于模块注释和文档注释之后,模块全局变量和常量之前。每一个导入语句只能导入一个模块。
  推荐:

1
2
3
import re
import struct
import binascii

  不推荐:

1
import re, struct, binascii

  但是如果from import后面跟有多个代码元素是可以的。

1
from codeop import CommandCompiler, compile_command

  导入语句应该按照从通用到特殊的顺序分组,顺序是:标准库→第三方库→自己的模块。每一组之间有一个空行,而且组中模块是按照英文字母顺序排序的。

4.代码排版

  代码排版包括空行、空格、断行和缩进等内容。

4.1 空行

  空行用以将逻辑相关的代码段分隔开,以提高可读性。
  1.import语句块前后保留两个空行。
  2.函数声明之前保留两个空行。
  3.类声明之前保留两个空行。
  4.方法声明之前保留一个空行。
  5.两个逻辑代码块之间应该保留一个空行。

4.2 空格

  代码中的有些位置是需要有空格的。
  1.赋值符号“=”前后各有一个空格。

1
2
a = 10
c = 10

  2.所有的二元运算符都应该使用空格与操作数分开。

1
a += c + d

  3.一元运算符:算法运算符取反“-”和运算符取反“~”。

1
2
3
b = 10
a = -b
y = ~b

  4.括号内不要有空格,Python中括号包括小括号“()”、中括号“[]”和大括号“{}”。
  推荐:

1
doque(cat[1], {dogs: 2}, [])

  不推荐:

1
doque(cat[ 1 ], { dogs: 2 }, [ ])

  5.不要在逗号、分号、冒号前面有空格,而是要在它们后面有一个空格,除非该符号已经是行尾了。
  推荐:

1
2
3
if x == 88:
print(x, y)
x, y = y, x

  不推荐:

1
2
3
if x == 88:
print(x , y)
x , y = y , x

  6.参数列表、索引或切片的左括号前不应有空格。
  推荐:

1
2
doque(1)
dogs['key'] = list[index]

  不推荐:

1
2
doque (1)
dict ['key'] = list [index]

  7.如果使用具有不同优先级的运算符,请考虑在具有最低优先级的运算符周围添加空格。有时需要通过自己来判断;但是,不要使用一个以上的空格,并且在二元运算符的两边使用相同数量的空格。

4.3 缩进

  4个空格常被作为缩进排版的一个级别。虽然在开发时程序员可以使用制表符进行缩进,而默认情况下一个制表符等于8个空格,但是不同的IDE工具中一个制表符与空格对应个数会有不同,所以不要使用制表符缩进。Python 3不允许同时使用空格和制表符的缩进。
  代码块的内容相当于首行缩进一个级别(4个空格)。

4.4 断行

  一行代码中最多79个字符,对于文档注释和多行注释时一行最多72个字符,但是如果注视中包含URL地址可以不受这个限制。否则,如果超过则需断行。
  1.在逗号后面断开。
  2.在运算符前面断开。
  3.尽量不要使用续行符“\”,当有括号(包括大括号、中括号和小括号)则在括号中断开,这样可以不使用续行符。有时为了省略续行符,会将表达式用小括号括起来。在括号中,续行是隐式的。
  4.反斜杠有时依然很有用。比如,比较长的,多个with状态语句,不能使用隐式续行,所以反斜杠是可以接受。
  5.续行应该与其包裹元素对齐,要么使用圆括号、方括号和花括号内的隐式行连接来垂直对齐,要么使用挂行缩进对齐。当使用挂行缩进时,应该考虑到第一行不应该有参数,以及使用缩进以区分自己是续行。四空格的规则对于续行是可选的。挂行缩进不一定要用4个空格。

5.源文件编码

  Python核心发布版本中的代码总是以UTF-8格式编码(或者在Python 2中用ASCII编码)。
  使用ASCII(在Python 2中)或UTF-8(在Python 3中)编码的文件不应具有编码声明。
  在标准库中,非默认的编码应该只用于测试,或者当一个注释或者文档字符串需要提及一个包含内ASCII字符编码的作者名字的时候;否则,使用\x,\u,\U , 或者 \N 进行转义来包含非ASCII字符。
  对于Python 3和更高版本,标准库规定了以下策略(参见 PEP 3131):Python标准库中的所有标识符必须使用ASCII标识符,并在可行的情况下使用英语单词(在许多情况下,缩写和技术术语是非英语的)。此外,字符串文字和注释也必须是ASCII。唯一的例外是测试非ASCII特征的测试用例以及作者的名称。作者的名字如果不使用拉丁字母拼写,必须提供一个拉丁字母的音译。

【算法积累】动态规划与斐波那契数列

题目

  注:本题来自LeetCode题库70.爬楼梯。

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。

  示例1:

1
2
3
4
5
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

  示例2:

1
2
3
4
5
6
 输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

解法1:动态规划法

  不难发现,这个问题可以被分解为一些包含最优子结构的子问题,即它的最优解可以从其子问题的最优解来有效地构建,我们可以使用动态规划来解决这一问题。
  假设我们要爬到第n个台阶,则有两种情况:一是爬到第(n-1)个台阶,然后再往上爬1个台阶即可;二是爬到第(n-2)个台阶,然后再往上爬2个台阶即可。也就是说,爬到第n个台阶的方法数,就是爬到第(n-1)个台阶的方法数与爬到第(n-2)个台阶的方法数之和。
  特别地,当n=1时,方法数为1;当n=2时,方法数为2(即1+1或直接爬2个台阶)。
  代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 语言:C
// 执行用时:0ms(又是0ms,具体情况未知)
// 内存消耗:6.8MB
int climbStairs(int n){
if (n == 1)
return 1;
int first, second, third, i;
first = 1;
second = 2;
for (i = 3; i < n + 1; i++)
{
third = first + second;
first = second;
second = third;
}
return second;
}

  这种解法的时间复杂度为O(n),空间复杂度为O(n)。

解法2:斐波那契数列法

  我们再分析一下解法1,不难发现这其实就是斐波那契数列。所以这道题还可以用斐波那契数列法来解决。下面给出C和Python 3的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 语言:C
// 执行用时:0ms(我不想再说什么了……为啥又是0啊QAQ……)
// 内存消耗:6.9MB
int climbStairs(int n){
if (n == 1)
return 1;
int fib[100];
fib[1] = 1;
fib[2] = 2;
int i;
for (i = 3; i <= n; i++)
fib[i] = fib[i - 1] + fib[i - 2];
return fib[n];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""
语言:Python 3
执行用时:36ms(诶它终于不是0ms了嘿嘿QvQ)
内存消耗:12.7MB
"""
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
fib = [1, 1, 2]
for i in range(3, n + 1):
next = fib[i - 1] + fib[i - 2]
fib.append(next)
return fib[n]

  同样地,这种解法的时间复杂度为O(n),空间复杂度为O(n)。

解法3:斐波那契公式法

  既然知道这道题本质上就是斐波那契数列了,那么我们就可以直接使用公式啦!
  斐波那契数列的公式如下:
$$F_n=\frac{1}{\sqrt{5}}\left(\left(\frac{1+\sqrt{5}}{2}\right)^{n+1}-\left(\frac{1-\sqrt{5}}{2}\right)^{n+1}\right)$$
  套用这个公式可以很快地解决本题。Python 3代码如下:

1
2
3
4
5
6
7
8
9
10
"""
语言:Python 3
执行用时:48ms(比上一个方法慢了不少诶QnQ)
内存消耗:12.8MB
"""
import math

class Solution:
def climbStairs(self, n: int) -> int:
return int((pow((1+math.sqrt(5))/2, n+1) - pow((1-math.sqrt(5))/2, n+1)) / math.sqrt(5))

  这种方法的时间复杂度为O(log(n))(pow()将会用去log(n)的时间),空间复杂度为O(1)(使用常量级空间)。

【算法积累】二分查找法

二分查找法简介

  二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
  首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
  算法要求:1.必须采用顺序存储结构。2.必须按关键字大小有序排列。
  比较次数的计算公式:a<log2n<b,其中a,b,n均为正整数。即当顺序表有n个关键字时:查找失败至少比较a次关键字,查找成功最多比较关键字次数是b。

二分查找法模板

  以Java为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = (left + right) / 2;
if(nums[mid] == target) {
// 相关逻辑
} else if(nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 相关返回值
return 0;
}
}

  或者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length;
while(left < right) {
int mid = (left + right) / 2;
if(nums[mid] == target) {
// 相关逻辑
} else if(nums[mid] < target) {
left = mid + 1;
} else {
right = mid; // 注意
}
}
// 相关返回值
return 0;
}
}

注:上述代码参考LeetCode用户灵魂画师牧码的题解画解算法:35.搜索插入位置。链接:link

实例

题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
注:该题目出自LeetCode题库35.搜索插入位置。

  这道题最容易想到的方法是遍历法。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 语言:C
// 执行用时:8ms
// 内存消耗:7.2MB
int searchInsert(int* nums, int numsSize, int target){
int i;
for (i = 0; i < numsSize; i++)
{
if (nums[i] >= target)
return i;
}
return i;
}

  应用二分查找法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 语言:Java
// 执行用时:0ms(具体原因未知,可能是因为提交的时候网络卡顿)
// 内存消耗:39.2MB
class Solution {
public int searchInsert(int[] nums, int target) {
int l = 0;
int r = nums.length - 1;
while (l <= r) {
int m = (l + r) / 2;
if (nums[m] == target) {
return m;
} else if (nums[m] > target) {
r = m - 1;
} else {
l = m + 1;
}
}
return l;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 语言:C
// 执行用时:8ms
// 内存消耗:7.1MB
int searchInsert(int* nums, int numsSize, int target){
int l, r;
l = 0;
r = numsSize - 1;
while (l <= r)
{
int m = (l + r) / 2;
if (nums[m] == target)
return m;
else if (nums[m] > target)
r = m - 1;
else
l = m + 1;
}
return l;
}

  当然,我们还可以使用Python无脑解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
"""
语言:Python3
执行用时:44ms
内存消耗:13.5MB
"""
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
if target in nums:
return nums.index(target)
else:
nums.append(target)
nums = sorted(nums)
return nums.index(target)

  在逛LeetCode评论区的时候,还看到了一位大神李达西的Python“一行流”解法:

1
2
3
4
5
6
7
8
"""
语言:Python3
执行用时:据该用户的说法是50ms
内存消耗:未知
"""
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
return len([i for i in nums if i < target])

【总结篇】Python matplotlib之使用统计函数绘制简单图形

写在前面

  作者注:我在这里只总结函数的功能及其用法,程序实例参考链接:link
  我们用下面的语句来导入matplotlib库:

1
import matplotlib.pyplot as plt

绘制简单图形的统计函数及其用法

1.函数bar()

函数bar()用来绘制柱状图。
函数功能:在x轴上绘制定性数据的分布特征。
调用签名:

1
plt.bar(x, y)

参数说明:
x:标示在x轴上的定性数据的类别。
y:每种定性数据的类别的数量。

2.函数barh()

函数barh()用于绘制条形图。
函数功能:在y轴上绘制定性数据的分布特征。
调用签名:

1
plt.barh(x, y)

参数说明:
x:标示在y轴上的定性数据的类别。
y:每种定性数据的类别的数量。

3.函数hist()

函数hist()用于绘制直方图。
函数功能:在x轴上绘制定量数据的分布特征。
调用签名:

1
plt.hist(x)

参数说明:
x:在x轴上绘制箱体的定量数据输入值。

4.函数pie()

函数pie()用于绘制饼图。
函数功能:绘制定性数据的不同类别的百分比。
调用签名:

1
plt.pie(x)

参数说明:
x:定性数据的不同类别的百分比。

5.函数polar()

函数polar()用于绘制极线图。
函数功能:在极坐标轴上绘制折线图。
调用签名:

1
plt.polar(theta, r)

参数说明:
theta:每个标记所在射线与极径的夹角。
r:每个标记到原点的距离。

6.函数scatter()

函数scatter()用于绘制气泡图。
函数功能:二位数据借助气泡大小展示三维数据。
调用签名:

1
plt.scatter(x, y)

参数说明:
x:x轴上的数值。
y:y轴上的数值。
s:散点标记的大小。
c:散点标记的颜色。
cmap:将浮点数映射成颜色的颜色映射表。

7.函数stem()

函数stem()用于绘制棉棒图。
函数功能:绘制离散有序数据。
调用签名:

1
plt.stem(x, y)

参数说明:
x:指定棉棒上的x轴基线上的位置。
y:绘制棉棒的长度。
linefmt:棉棒的样式。
markerfmt:棉棒末端的样式。
basefmt:指定基线的样式。

8.函数boxplot()

函数boxplot()用于绘制箱线图。
函数功能:绘制箱线图。
调用签名:

1
plt.boxplot(x)

参数说明:
x:绘制箱线图的输入数据。

9.函数errorbar()

函数errorbar()用于绘制误差棒图。
函数功能:绘制y轴方向或是x轴方向的误差范围。
调用签名:

1
plt.errorbar(x, y, yerr=a, xerr=b)

参数说明:
x:数据点的水平位置。
y:数据点的垂直位置。
yerr:y轴方向的数据点的误差计算方法。
xerr:x轴方向的数据点的误差计算方法。

写在最后

  如果你有什么想法、意见和建议的话,欢迎联系我。
  我的邮箱:1398635912@qq.com

【总结篇】Python matplotlib之使用函数绘制matplotlib的图表组成元素

写在前面

学习参考书籍:《Python数据可视化之matplotlib实践》
出版社信息:中国工信出版集团 电子工业出版社
作者:刘大成
ISBN:978-7-121-34888-4

作者注:我在这里只总结函数的功能及其用法,程序实例参考链接:link

matplotlib简介

  matplotlib库是Python中绘制二维、三维图表的数据可视化工具。它能让使用者很轻松地将数据图形化,并且提供多样化的输出格式。
  在使用matplotlib库的时候,我们应先用下面的语句导入matplotlib库:

1
2
import matplotlib as mpl
import matplotlib.pyplot as plt

绘制图表组成元素的函数及其用法

1.函数plot()

函数功能:展现变量的趋势变化。
调用签名:

1
plt.plot(s, y, ls="-", lw=2, label="plot figure")

参数说明:
x:x轴上的数值。
y:y轴上的数值。
ls:折线图的线条风格。
lw:折线图的线条宽度。
label:标记图形内容的标签文本。

2.函数scatter()

函数功能:寻找变量之间的关系。
调用签名:

1
plt.scatter(x, y, c="b", label="scatter figure")

参数说明:
x:x轴上的数值。
y:y轴上的数值。
c:散点图中标记的颜色。
label:标记图形内容的标签文本。

3.函数xlim() & ylim()

函数功能:设置x轴(y轴)的数值显示范围。
调用签名:

1
plt.xlim(xmin, xmax)

参数说明:
xmin:x轴上的最小值。
xmax:x轴上的最大值。
平移性:上面的函数功能,调用签名和参数说明同样可以平移到函数ylim()上。

4.函数xlabel() & ylabel()

函数功能:设置x轴(y轴)的标签文本。
调用签名:

1
plt.xlabel(string)

参数说明:
string:标签文本内容。
平移性:上面的函数功能,调用签名和参数说明同样可以平移到函数ylabel()上。

5.函数grid()

函数功能:绘制刻度线的网格线。
调用签名:

1
ply.grid(linestyle=":", color="r")

参数说明:
linestyle:网格线的线条风格。
color:网格线的线条颜色。

6.函数axhline() & axvline()

函数功能:绘制平行于x轴(y轴)的水平(竖直)参考线。
调用签名:

1
plt.axhline(y=0.0, c="r", ls="--", lw=2)

参数说明:
y:水平参考线的出发点。
c:参考线的线条颜色。
ls:参考线的线条风格。
lw:参考线的线条宽度。
平移性:上面的函数功能,调用签名和参数说明同样可以平移到函数axvline()上。

7.函数axvspan() & axhspan()

函数功能:绘制垂直于x轴(y轴)的参考区域。
调用签名:

1
plt.avxspan(xmin=1.0, xmax=2.0, facecolor="y", alpha=0.3)

参数说明:
xmin:参考区域的起始位置。
xmax:参考区域的终止位置。
facecolor:参考区域的填充颜色。
alpha:参考区域的填充颜色的透明度。
平移性:上面的函数功能、调用签名和参数说明可以平移到函数axhspan()上。

8.函数annotate()

函数功能:添加图形内容细节的指向型注释文本。
调用签名:

1
2
3
4
5
plt.annotate(string, xy=(np.pi/2, 1.0),
xytext=((np.pi/2)+0.15, 1.5),
weight="bold",
color="b",
arrowprops=dict(arrowstyle="->", connectionstyle="arc3", color="b"))

参数说明:
string:图形内容的注释文本。
xy:被注释图形内容的位置坐标。
xytext:注释文本的位置坐标。
weight:注释文本的字体粗细风格。
color:注释文本的字体颜色。
arrowprops:指示被注释内容的箭头的属性字典。

9.函数text()

函数功能:添加图形内容细节的无指向性注释文本。
调用签名:

1
plt.text(x, y, string, weight="bold", color="b")

参数说明:
x:注释文本内容所在位置的横坐标。
y:注释文本内容所在位置的纵坐标。
string:注释文本内容。
weight:注释文本内容的粗细风格。
color:注释文本内容的字体颜色。

10.函数title()

函数功能:添加图形内容的标题。
调用签名:

1
plt.title(string)

参数说明:
string:图形内容的标题文本。

11.函数legend()

函数功能:标示不同图形的文本标签图例。
调用签名:

1
plt.legend(loc="lower left")

参数说明:
loc:图例在图中的地理位置。

12.函数show()

函数功能:在屏幕上显示出绘制的图表。
调用签名:

1
plt.show()

写在最后

  我最近刚刚开始学习Python的matplotlib库,对此的感觉就是函数及其参数很多、很不好记。但是matplotlib真的是一个功能很强大的库,在数据可视化领域的重要性不言而喻。希望自己可以通过这一段时间的学习熟练掌握matplotlib库的用法。
  如果你有什么想法、意见和建议的话,欢迎联系我。
  我的邮箱:1398635912@qq.com

递归法解汉诺塔问题

汉诺塔简介

  法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔(Tower of Hanoi)。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
  我们假设金片的数量为n,移动的次数为f(n),不难发现f(k+1)=2f(k)+1,也不难证明出f(n)=2^n^-1。也就是说,移动64片金片至少需要(2^64^-1)次,也就是18,446,744,073,709,551,615‬次。假设每秒钟移动一个圆盘,不考虑闰年,移动这些金片也需要584,942,417,355年。要知道,地球的年龄是45.5亿年……

递归法解汉诺塔问题

  以上所说的属于数学范畴,在此不再赘述。我们要讲的重点是用代码来解决汉诺塔问题。这里我采用了递归的思想。递归并不一定是最好的方法,但就我而言,递归是最容易想到也是最容易理解的方法。
  我们设汉诺塔的三根柱子从左到右分别为A,B,C,在A柱上有n个圆盘(PS:还是叫圆盘好听些,叫金片总是感觉有些不习惯……)。由于汉诺塔只能把小圆盘放在大圆盘上,想要把A柱的所有圆盘都移动到C柱,必须把最大圆盘上面的全部圆盘都移动到B柱,然后才可以把最大的圆盘移动到C柱。我们可以总结步骤如下:
  第一步:把(n-1)个圆盘都从A柱移动到B柱;
  第二步:把最大的圆盘从A柱移动到C柱;
  第三步:把(n-1)个圆盘都从B柱移动到C柱。
  特别地,当n=1时,只需将圆盘从A柱移动到C柱即可。
  Python代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
def hanoi(n, a, b, c):
if n == 1:
print(a, '->', c)
return
hanoi(n - 1, a, c, b)
hanoi(1, a, b, c) # 这里也可以写成print(a, '->', c)
hanoi(n - 1, b, a, c)


num = int(input('请输入圆盘数量:'))
print('移动方法如下:')
hanoi(num, 'A', 'B', 'C')

  取圆盘数量为4,得到输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
请输入圆盘数量:4
移动方法如下:
A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B
A -> C
B -> C
B -> A
C -> A
B -> C
A -> B
A -> C
B -> C

C语言经典问题——兑换硬币

兑换硬币问题

  兑换硬币问题是C语言的一个经典问题。题目如下:

现有一张1元纸币,欲将其兑换为1分、2分、5分硬币共60枚,请列出所有兑换方案。

  我们可以利用分支和循环来解决这个问题。

最简单的方法——三重循环法

  最“无脑”也是最容易想到的方法是利用三重循环。其代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
int main (void)
{
int i = 0;
int one, two, five;
for (one = 0; one <= 60; one++)
{
for (two = 0; two <= 50; two++)
{
for (five = 0; five <= 20; five++)
{
if ((one * 1 + two * 2 + five * 5 == 100) && (one + two + five == 60))
{
i++;
printf ("兑换方案%2d:1分硬币%2d枚,2分硬币%2d枚,5分硬币%2d枚\n", i, one, two, five);
}
}
}
}
printf ("共有%d种兑换方案\n", i);
return 0;
}

  输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
兑换方案 1:1分硬币20枚,2分硬币40枚,5分硬币 0枚
兑换方案 2:1分硬币23枚,2分硬币36枚,5分硬币 1枚
兑换方案 3:1分硬币26枚,2分硬币32枚,5分硬币 2枚
兑换方案 4:1分硬币29枚,2分硬币28枚,5分硬币 3枚
兑换方案 5:1分硬币32枚,2分硬币24枚,5分硬币 4枚
兑换方案 6:1分硬币35枚,2分硬币20枚,5分硬币 5枚
兑换方案 7:1分硬币38枚,2分硬币16枚,5分硬币 6枚
兑换方案 8:1分硬币41枚,2分硬币12枚,5分硬币 7枚
兑换方案 9:1分硬币44枚,2分硬币 8枚,5分硬币 8枚
兑换方案10:1分硬币47枚,2分硬币 4枚,5分硬币 9枚
兑换方案11:1分硬币50枚,2分硬币 0枚,5分硬币10枚
共有11种兑换方案

稍加改进——双重循环法

  通过分析上面的程序,我们可以发现,通过利用变量one,two和five之间的关系,即one+two+five=60,我们可以减少一层循环,从而达到代码的优化。改进后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int main(void)
{
int i = 0;
int one, two, five;
for (five = 0; five < 20; five++)
{
for (two = 0; two < 50; two++)
{
one = 60 - two - five;
if (one + two * 2 + five * 5 == 100)
{
i++;
printf ("兑换方案%2d:1分硬币%2d枚,2分硬币%2d枚,5分硬币%2d枚\n", i, one, two, five);
}
}
}
printf ("共有%d种兑换方案\n", i);
return 0;
}

  输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
兑换方案 1:1分硬币20枚,2分硬币40枚,5分硬币 0枚
兑换方案 2:1分硬币23枚,2分硬币36枚,5分硬币 1枚
兑换方案 3:1分硬币26枚,2分硬币32枚,5分硬币 2枚
兑换方案 4:1分硬币29枚,2分硬币28枚,5分硬币 3枚
兑换方案 5:1分硬币32枚,2分硬币24枚,5分硬币 4枚
兑换方案 6:1分硬币35枚,2分硬币20枚,5分硬币 5枚
兑换方案 7:1分硬币38枚,2分硬币16枚,5分硬币 6枚
兑换方案 8:1分硬币41枚,2分硬币12枚,5分硬币 7枚
兑换方案 9:1分硬币44枚,2分硬币 8枚,5分硬币 8枚
兑换方案10:1分硬币47枚,2分硬币 4枚,5分硬币 9枚
兑换方案11:1分硬币50枚,2分硬币 0枚,5分硬币10枚
共有11种兑换方案

  我们得到了相同的输出结果,可见这种想法是可行的。

最优方法——单重循环法

  实际上,这道题只用单重循环也是可以解决的。
  我们再次对变量one,two和five之间的关系进行分析,不难得出以下两个关系式:

one + two + five = 60
one + two × 2 + five × 5 = 100

  整理之后,得:

two + five × 4 = 40,即 two = 40 - five × 4
five ≤ 10

  根据上面的分析,我们可以再次优化代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main(void)
{
int i = 0;
int one, two, five;
for (five = 0; five <= 10; five++)
{
two = 40 - 4 * five;
one = 60 - two - five;
if (one + two * 2 + five * 5 == 100)
{
i++;
printf ("兑换方案%2d:1分硬币%2d枚,2分硬币%2d枚,5分硬币%2d枚\n", i, one, two, five);
}
}
printf ("共有%d种兑换方案\n", i);
return 0;
}

  输出结果依旧和上面的两个程序相同:

1
2
3
4
5
6
7
8
9
10
11
12
兑换方案 1:1分硬币20枚,2分硬币40枚,5分硬币 0枚
兑换方案 2:1分硬币23枚,2分硬币36枚,5分硬币 1枚
兑换方案 3:1分硬币26枚,2分硬币32枚,5分硬币 2枚
兑换方案 4:1分硬币29枚,2分硬币28枚,5分硬币 3枚
兑换方案 5:1分硬币32枚,2分硬币24枚,5分硬币 4枚
兑换方案 6:1分硬币35枚,2分硬币20枚,5分硬币 5枚
兑换方案 7:1分硬币38枚,2分硬币16枚,5分硬币 6枚
兑换方案 8:1分硬币41枚,2分硬币12枚,5分硬币 7枚
兑换方案 9:1分硬币44枚,2分硬币 8枚,5分硬币 8枚
兑换方案10:1分硬币47枚,2分硬币 4枚,5分硬币 9枚
兑换方案11:1分硬币50枚,2分硬币 0枚,5分硬币10枚
共有11种兑换方案