文章目录
程序 = 数据结构 + 算法.数据结构用于描述数据,算法基于数据结构操作数据.
11.1 算法及其性能分析
11.1.1 算法概述
算法是解决问题的一种方法或一个过程.
算法的性质:
-
确定性:对于同样的输入,算法总是得出相同的输出结果,不会出现随机性。
-
可行性:算法必须是可行的,即能够在有限的时间和内存空间内完成执行。
-
输入与输出:算法必须有明确的输入和输出,且输出应与输入有明确的关系。
-
有限性:算法必须在有限的步骤内停止。
算法的性能包括:
-
时间复杂度:算法执行所需的时间与问题规模之间的关系。
-
空间复杂度:算法执行所需的内存空间与问题规模之间的关系。
-
最优性:算法是否能够得到问题的最优解。
-
可读性:算法是否易于理解、修改和维护。
-
可靠性:算法是否能够正确地解决问题,不会出现错误或异常情况。
-
可扩展性:算法是否能够处理更大规模的问题或更复杂的情况。
在实际应用中,需要根据具体问题的特点和要求,综合考虑算法的不同性能指标,选择合适的算法。例如,对于需要快速处理大规模数据的问题,通常需要选择时间复杂度较低的算法;而对于需要解决精确问题的问题,则需要选择能够得到最优解的算法。
11.1.2 算法的时间复杂度分析
算法的时间复杂度是衡量算法性能的重要指标之一,它描述了算法执行时间与问题规模之间的增长关系。
通常情况下,我们使用“大 O 记法”(Big-O notation)来表示算法的时间复杂度。假设算法执行时间为 T(n),其中 n 表示问题规模,我们使用 O(f(n)) 表示算法的时间复杂度,表示 T(n) 最多增长到 f(n) 的级别。其中,f(n) 是一个函数,表示算法的增长率。
在分析算法时间复杂度时,我们通常关注算法的最坏情况时间复杂度。也就是说,我们假设算法在处理最坏情况下的输入时,所需的执行时间是最长的。
常见的时间复杂度包括:
-
O(1):常数时间复杂度,表示算法的执行时间与问题规模无关,执行时间是固定的。
-
O(log n):对数时间复杂度,表示算法的执行时间随着问题规模增长而增长,但是增长非常缓慢,通常是二分查找等算法的时间复杂度。
-
O(n):线性时间复杂度,表示算法的执行时间随着问题规模线性增长,通常是遍历数组等算法的时间复杂度。
-
O(nlog n):线性对数时间复杂度,表示算法的执行时间随着问题规模增长而增长,但是增长速度比 O(n) 快,通常是快速排序等算法的时间复杂度。
-
O(n²):平方时间复杂度,表示算法的执行时间随着问题规模的增长而呈二次方增长,通常是冒泡排序、插入排序等算法的时间复杂度。
-
O(2ⁿ):指数时间复杂度,表示算法的执行时间随着问题规模的增长呈指数级增长,通常是暴力穷举等算法的时间复杂度。
在实际应用中,我们需要根据具体问题的特点和要求,选择合适的算法,使得算法的时间复杂度能够满足问题的需求,并且在合理的时间内得到结果。
11.1.3 增长量级
增长量级是用来描述算法的时间复杂度的一个概念,它表示算法执行时间随着问题规模的增加而增加的速度。我们通常使用大 O 记法来表示算法的增长量级。
假设算法 A 和算法 B 都是用来解决同一个问题的,以下是它们在不同问题规模下的运行时间对照表:
问题规模(n) | 算法 A 运行时间 | 算法 B 运行时间 |
---|---|---|
10 | 1 毫秒 | 2 毫秒 |
100 | 10 毫秒 | 20 毫秒 |
1000 | 100 毫秒 | 200 毫秒 |
10000 | 1 秒 | 2 秒 |
100000 | 10 秒 | 20 秒 |
假设我们使用大 O 记法来表示算法 A 和算法 B 的时间复杂度,分别为 O(f(n)) 和 O(g(n))。则根据上表中的运行时间对比,我们可以得到以下结论:
-
对于相同问题规模,算法 A 的运行时间总是小于算法 B 的运行时间。
-
在问题规模很小的情况下,算法 A 和算法 B 的运行时间差异不大,可以视为常数级别的时间复杂度。
-
随着问题规模的增加,算法 A 的运行时间增长速度比算法 B 慢,因此算法 A 的时间复杂度低于算法 B 的时间复杂度。
具体来说,如果算法 A 的时间复杂度为 O(f(n)),算法 B 的时间复杂度为 O(g(n)),且 f(n) < g(n),则可以说算法 A 的增长量级小于算法 B 的增长量级。
函数类型 | 增长量级 | 举例 |
---|---|---|
常数函数 | O(1) | 返回单个变量的赋值操作 |
对数函数 | O(log n) | 二分查找算法 |
线性函数 | O(n) | 遍历数组算法 |
线性对数函数 | O(n log n) | 快速排序算法 |
平方函数 | O(n²) | 冒泡排序、插入排序 |
立方函数 | O(n³) | 矩阵运算 |
指数函数 | O(2ⁿ) | 暴力穷举算法 |
需要注意的是,上述表格中的举例并不是全部,仅为常见的一些例子。实际上,不同类型的算法可能会有不同的时间复杂度,因此在分析算法的时间复杂度时,需要具体问题具体分析。同时,需要注意算法的时间复杂度和具体实现之间的关系,因为具体实现的常数因素也会影响算法的执行时间。
11.1.4 算法的空间复杂度分析
算法的空间复杂度是指算法在运行过程中所需要的内存空间大小,通常用算法输入规模 n 的函数 S(n) 来表示。
类似于时间复杂度分析,我们需要根据算法的实际执行过程来分析其空间复杂度。常见的空间复杂度分析方法包括:
-
常数空间复杂度:O(1),表示算法的空间复杂度不随问题规模的增加而增加,通常是一些只使用常数个变量的算法。
-
线性空间复杂度:O(n),表示算法的空间复杂度随问题规模的增加而呈线性增长,通常是一些需要使用数组等数据结构的算法。
-
线性对数空间复杂度:O(n log n),表示算法的空间复杂度随问题规模的增加而增加,但增长速度比线性空间复杂度慢,通常是一些需要使用二叉堆、红黑树等数据结构的算法。
-
平方空间复杂度:O(n²),表示算法的空间复杂度随问题规模的增加而呈平方级别增长,通常是一些需要使用二维数组等数据结构的算法
-
常数空间复杂度:O(1)
例如一些只使用常数个变量的算法,如交换两个变量的值或者计算斐波那契数列的第 n 项。
-
线性空间复杂度:O(n)
例如需要使用数组等数据结构的算法,如顺序查找算法或者数组反转操作。
-
线性对数空间复杂度:O(n log n)
例如需要使用二叉堆、红黑树等数据结构的算法,如堆排序或者归并排序。
-
平方空间复杂度:O(n²)
例如需要使用二维数组等数据结构的算法,如动态规划算法或者矩阵运算。
11.2 查找算法
11.2.1 顺序查找法
顺序查找法是一种简单的查找算法,其时间复杂度和空间复杂度分析如下:
时间复杂度:
在最坏情况下,需要遍历整个数组才能找到要查找的元素,因此时间复杂度为 O(n)。
在最好情况下,要查找的元素恰好在数组的第一个位置,只需要一个比较操作就可以找到,因此时间复杂度为 O(1)。
平均情况下,我们假设要查找的元素在数组中的任意位置概率相等,那么需要比较的次数为:(1+2+…+n)/n = (n+1)/2,因此时间复杂度为 O(n)。
空间复杂度:
顺序查找法只需要常数个额外空间来存储查找过程中的一些变量,因此空间复杂度为 O(1)。
需要注意的是,虽然顺序查找法的时间复杂度比较高,但其实现简单、易于理解,对于小规模的数据集来说是一种不错的查找算法。而对于较大规模的数据集来说,更适合使用时间复杂度更低的二分查找等算法。
11.2.2 二分查找法
二分查找法又称折半查找法,用于已经排序列表的查找问题.
二分查找法(Binary Search)是一种基于比较目标值和数组中间元素的查找算法,适用于有序数组。其过程如下:
-
首先,将目标值与有序数组的中间元素进行比较。
-
如果目标值等于中间元素,则查找成功,返回中间元素的下标。
-
如果目标值小于中间元素,则在数组左半部分继续进行二分查找法。
-
如果目标值大于中间元素,则在数组右半部分继续进行二分查找法。
-
重复以上步骤,直到找到目标值或者数组被搜索完毕。
如果最终没有找到目标值,则返回一个特定的值(通常是 -1)表示查找失败。
二分查找法的时间复杂度为 O(log n),其中 n 表示数组的长度。由于每次查找都会将搜索范围缩小一半,因此在最坏情况下,需要进行 log n 次比较才能找到目标元素。因此,当数组长度很大时,二分查找法比顺序查找法具有更高的效率。
需要注意的是,二分查找法要求数组是有序的,因此如果数组无序,则需要先进行排序操作。同时,二分查找法也比较容易被实现错误,因此在实现时需要特别注意边界条件。
- 二分查找法的递归实现:
def _binarySearch(key, a, lo, hi):
if hi <= lo: return -1 #查找失败,返回-1
mid = (lo + hi) // 2 #计算中间位置
if a[mid] > key: #中间位置项目大于查找关键字
return _binarySearch(key, a, lo, mid) #递归查找前一子表
elif a[mid] < key: #中间位置项目小于查找关键字
return _binarySearch(key, a, mid+1, hi) #递归查找后一子表
else: #中间位置项目等于查找关键字
return mid #查找成功,返回下标位置
def binarySearch(key, a): #二分查找
return _binarySearch(key, a, 0, len(a)) #递归二分查找法
def main():
a = [1,13,26,33,45,55,68,72,83,99]
print("关键字位于列表索引",binarySearch(33, a)) #二分查找关键字33
print("关键字位于列表索引",binarySearch(58, a)) #二分查找关键字58
if __name__ == '__main__': main()
关键字位于列表索引 3 关键字位于列表索引 -1
- 二分查找法的非递归实现
def binarySearch(key, a): #二分查找法的非递归实现
low = 0 #左边界
high = len(a) - 1 #右边界
while low <= high: #左边界小于等于右边界,则循环
mid = (low + high) // 2 #计算中间位置
if a[mid] < key: #中间位置项目小于查找关键字
low = mid + 1 #调整左边界(在后一子表查找)
elif a[mid] > key: #中间位置项目大于查找关键字
high = mid - 1 #调整右边界(在前一子表查找)
else: #中间位置项目等于查找关键字
return mid #查找成功,返回下标位置
return -1 #查找不成功(不存在关键字),返回-1
def main():
a = [1,13,26,33,45,55,68,72,83,99]
print("关键字位于列表索引",binarySearch(33, a)) #二分查找关键字33
print("关键字位于列表索引",binarySearch(58, a)) #二分查找关键字58
if __name__ == '__main__': main()
关键字位于列表索引 3 关键字位于列表索引 -1
11.2.3 Python语言提供的查找算法
Python语言提供了多种内置的查找算法,包括:
- 线性查找(Linear Search):Python中可以使用 in 运算符或者 index() 方法实现线性查找,时间复杂度为 O(n)。
- 二分查找(Binary Search):Python中可以使用 bisect 模块提供的 bisect_left() 或者 bisect_right() 函数实现二分查找,时间复杂度为 O(log n)。这些函数只能用于有序序列。
- 哈希表(Hash Table)查找:Python中可以使用 dict 字典来实现哈希表查找,平均时间复杂度为 O(1)。但是,哈希表查找的时间复杂度可能会受到哈希函数的影响,因此需要选择适当的哈希函数。
- B树(B-Tree)查找:Python中可以使用第三方库 blist 提供的 sortedlist 类来实现 B树查找,时间复杂度为 O(log n)。
下面举例说明 Python 中内置的一些查找算法:
-
线性查找:
我们可以使用 in 运算符或者 index() 方法来实现线性查找,例如:
# 使用 in 运算符实现线性查找 lst = [1, 2, 3, 4, 5] if 3 in lst: print("Found") else: print("Not found") # 使用 index() 方法实现线性查找 lst = [1, 2, 3, 4, 5] try: idx = lst.index(3) print("Found at index", idx) except ValueError: print("Not found") ```
-
二分查找:
我们可以使用 bisect 模块提供的 bisect_left() 或者 bisect_right() 函数来实现二分查找,例如:
import bisect # 使用 bisect_left() 函数实现二分查找 lst = [1, 2, 3, 4, 5] idx = bisect.bisect_left(lst, 3) if idx < len(lst) and lst[idx] == 3: print("Found at index", idx) else: print("Not found") # 使用 bisect_right() 函数实现二分查找 lst = [1, 2, 3, 4, 5] idx = bisect.bisect_right(lst, 3) if idx > 0 and lst[idx-1] == 3: print("Found at index", idx-1) else: print("Not found") ```
-
哈希表查找:
我们可以使用 dict 字典来实现哈希表查找,例如:
# 使用 dict 字典实现哈希表查找 d = {'a': 1, 'b': 2, 'c': 3} if 'c' in d: print("Found") else: print("Not found") ```
-
B树查找:
我们可以使用第三方库 blist 提供的 sortedlist 类来实现 B树查找,例如:
from blist import sortedlist # 使用 sortedlist 类实现 B树查找 lst = sortedlist([1, 2, 3, 4, 5]) idx = lst.bisect_left(3) if idx < len(lst) and lst[idx] == 3: print("Found at index", idx) else: print("Not found") ```
需要注意的是,以上代码仅为示例,实际应用中需要根据具体问题进行合理选择。同时,需要注意不同查找算法的时间复杂度和空间复杂度之间的差异。
11.3 排序算法
11.3.1 冒泡排序法(Bubble Sort)
是一种简单的排序算法,其基本思想是通过相邻元素之间的比较和交换,将待排序序列中较大的元素逐步“冒泡”到序列的尾部,从而实现排序的过程。
具体来说,冒泡排序法的过程如下:
-
比较相邻的两个元素。如果第一个元素比第二个元素大,就交换它们的位置。
-
对每一对相邻元素重复上述操作,从序列的开头一直到结尾,这样一趟比较后,序列的最后一个元素就是序列中最大的元素。
-
针对所有未排序元素重复上述操作,每次比较都会将当前未排序区间的最大元素“冒泡”到该区间的最后面。
-
重复上述步骤,直到整个序列都有序。
冒泡排序法的时间复杂度为 O(n²),其中 n 表示待排序序列的长度。在最坏情况下,需要进行 n*(n-1)/2 次比较和交换,因此时间复杂度为 O(n²)。在最好情况下,待排序序列已经有序,只需要进行 n-1 次比较,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只需要常数个额外空间来存储一些辅助变量。
需要注意的是,冒泡排序法虽然算法简单,但效率较低,不适用于大规模数据的排序。通常情况下,更适合使用时间复杂度更低的排序算法,如快速排序、归并排序等。
def bubbleSort(a):
for i in range(len(a)-1,0,-1): #外循环
for j in range(i): #内循环
if a[j] > a[j + 1]: #大数往下沉
a[j], a[j + 1] = a[j + 1], a[j]
#print(a) #跟踪调试
def main():
a = [2,97,86,64,50,80,3,71,8,76]
bubbleSort(a)
print(a)
if __name__ == '__main__': main()
[2, 3, 8, 50, 64, 71, 76, 80, 86, 97]
11.3.2 选择排序法
选择排序(Selection Sort)是一种简单的排序算法,其基本思想是通过重复从未排序的部分中选出最小元素并将其放置到已排序部分的末尾,从而实现排序的过程。
具体来说,选择排序的过程如下:
-
在未排序序列中找到最小元素,存放到排序序列的起始位置。
-
从剩余未排序元素中继续寻找最小元素,放到已排序序列的末尾。
-
重复上述步骤,直到整个序列都有序。
选择排序的时间复杂度为 O(n²),其中 n 表示待排序序列的长度。在最坏情况下,需要进行 n(n-1)/2 次比较和 n-1 次交换,因此时间复杂度为 O(n²)。在最好情况下,待排序序列已经有序,只需要进行 n-1 次比较和 0 次交换,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只需要常数个额外空间来存储一些辅助变量。
需要注意的是,选择排序虽然算法简单,但效率较低,不适用于大规模数据的排序。通常情况下,更适合使用时间复杂度更低的排序算法,如快速排序、归并排序等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gv8NeFOS-1686328461855)(C:\Users\29363\Desktop\笔记\python\img\选择排序.png)]
def selectionSort(a):
for i in range(0, len(a)): #外循环(0~N-1)
m = i #当前位置下标
for j in range(i + 1, len(a)): #内循环
if a[j] < a[m]: #查找最小值的位置
m = j
a[i], a[m] = a[m], a[i] #元素交换
#print(a) #跟踪调试
def main():
a = [59,12,77,64,72,69,46,89,31,9]
selectionSort(a)
print(a)
if __name__ == '__main__': main()
[9, 12, 31, 46, 59, 64, 69, 72, 77, 89]
11.3.3 插入排序法
插入排序(Insertion Sort)是一种简单的排序算法,其基本思想是通过将待排序序列分为已排序和未排序两部分,将未排序部分的第一个元素插入到已排序部分的合适位置,从而实现排序的过程。
具体来说,插入排序的过程如下:
-
将待排序序列分为已排序和未排序两部分,初始化时已排序部分只有一个元素,即待排序序列的第一个元素。
-
遍历未排序部分的所有元素,将每个元素按照大小顺序插入到已排序部分的合适位置。
-
重复上述步骤,直到整个序列都有序。
插入排序的时间复杂度为 O(n²),其中 n 表示待排序序列的长度。在最坏情况下,需要进行 n(n-1)/2 次比较和 n(n-1)/2 次移动,因此时间复杂度为 O(n²)。在最好情况下,待排序序列已经有序,只需要进行 n-1 次比较和 0 次移动,因此时间复杂度为 O(n)。空间复杂度为 O(1),因为只需要常数个额外空间来存储一些辅助变量。
需要注意的是,插入排序虽然算法简单,但效率较低,不适用于大规模数据的排序。通常情况下,更适合使用时间复杂度更低的排序算法,如快速排序、归并排序等。
def insertSort(a):
for i in range(1, len(a)): #外循环(1~N-1)
j = i
while (j > 0) and (a[j] < a[j-1]): #内循环
a[j], a[j-1] = a[j-1], a[j] #元素交换
j -= 1 #继续循环
#print(a) #跟踪调试
def main():
a = [59,12,77,64,72,69,46,89,31,9]
insertSort(a)
print(a)
if __name__ == '__main__': main()
[12, 59, 77, 64, 72, 69, 46, 89, 31, 9] [12, 59, 77, 64, 72, 69, 46, 89, 31, 9] [12, 59, 64, 77, 72, 69, 46, 89, 31, 9] [12, 59, 64, 72, 77, 69, 46, 89, 31, 9] [12, 59, 64, 69, 72, 77, 46, 89, 31, 9] [12, 46, 59, 64, 69, 72, 77, 89, 31, 9] [12, 46, 59, 64, 69, 72, 77, 89, 31, 9] [12, 31, 46, 59, 64, 69, 72, 77, 89, 9] [9, 12, 31, 46, 59, 64, 69, 72, 77, 89] [9, 12, 31, 46, 59, 64, 69, 72, 77, 89]
11.3.4 归并排序法
归并排序(Merge Sort)是一种基于分治思想的排序算法,其基本思想是将待排序序列分为若干个子序列,每个子序列都是有序的,然后再将有序的子序列合并成一个有序的序列,从而实现排序的过程。
具体来说,归并排序的操作步骤如下:
-
将待排序序列分成两个子序列,分别对这两个子序列进行归并排序,直到每个子序列中只有一个元素为止。
-
将两个已经有序的子序列合并成一个有序的序列。具体操作是,比较两个子序列的第一个元素,将较小的元素放入合并后的序列中,并将其从原子序列中删除,再继续比较两个子序列的第一个元素,依次类推,直到所有元素都放入了合并后的序列中。
-
重复上述步骤,直到所有子序列都被合并成一个有序的序列。
归并排序的时间复杂度为 O(nlogn),其中 n 表示待排序序列的长度。在最坏情况下,需要进行 logn 层递归,每层需要进行 n 次比较和移动,因此时间复杂度为 O(nlogn)。空间复杂度为 O(n),因为需要额外的空间来保存归并后的有序序列。
需要注意的是,归并排序虽然时间复杂度比冒泡排序、选择排序和插入排序高,但其稳定性好且适用于大规模数据的排序,因此在实际应用中被广泛使用。
def merge(left, right): #合并两个列表
merged = []
i, j = 0, 0 #i和j分别作为left和right的下标
left_len, right_len = len(left), len(right) #分别获取左右子列表的长度
while i < left_len and j < right_len: #循环归并左右子列表元素
if left[i] <= right[j]:
merged.append(left[i]) #归并左子列表元素
i += 1
else:
merged.append(right[j]) #归并右子列表元素
j += 1
merged.extend(left[i:]) #归并左子列表剩余元素
merged.extend(right[j:]) #归并右子列表剩余元素
#print(left,right,merged) #跟踪调试
return merged #返回归并好的列表
def mergeSort(a): #归并排序
if len(a) <= 1: #空或者只有1个元素,直接返回列表
return a
mid = len(a) // 2 #列表中间位置
left = mergeSort(a[:mid]) #归并排序左子列表
right = mergeSort(a[mid:]) #归并排序右子列表
return merge(left, right) #合并排好序的左右两个子列表
def main():
a = [59,12,77,64,72,69,46,89,31,9]
a1 = mergeSort(a)
print(a1)
if __name__ == '__main__': main()
[9, 12, 31, 46, 59, 64, 69, 72, 77, 89]
11.3.5 快速排序法
快速排序(Quick Sort)是一种基于分治思想的排序算法,其基本思想是通过选取一个基准元素,将待排序序列分成两个子序列,其中一个子序列中的所有元素都小于等于基准元素,另一个子序列中的所有元素都大于等于基准元素,然后对子序列进行递归排序,从而实现排序的过程。
具体来说,快速排序的操作步骤如下:
-
选取一个基准元素,一般是待排序序列的第一个元素或最后一个元素。
-
将待排序序列分成两个子序列,其中一个子序列中的所有元素都小于等于基准元素,另一个子序列中的所有元素都大于等于基准元素。具体操作是,从待排序序列的两端开始,分别向中间扫描,当左端的元素大于等于基准元素时停止扫描,当右端的元素小于等于基准元素时停止扫描,然后交换左端和右端的元素,继续扫描,直到左端的元素都小于基准元素,右端的元素都大于基准元素。
-
对两个子序列进行递归排序,直到所有子序列都有序。
快速排序的时间复杂度为 O(nlogn),其中 n 表示待排序序列的长度。在最坏情况下,待排序序列已经有序或元素全部相等,每次递归只能划分出一个子序列,需要进行 n-1 次递归,因此时间复杂度为 O(n²)。在最好情况下,每次递归都可以划分出两个长度为 n/2 的子序列,需要进行 logn 层递归,因此时间复杂度为 O(nlogn)。空间复杂度为 O(logn),因每次递归需要额外的空间来保存基准元素的位置。
需要注意的是,快速排序虽然时间复杂度比冒泡排序、选择排序和插入排序高,但其平均效率高且适用于大规模数据的排序,因此在实际应用中被广泛使用。
def quickSort(a, low, high): #对列表a快速排序,列表下界为low,上界为high
i = low #i等于列表下界
j = high #j等于列表上界
if i >= j: #如果下界大于等于上界,返回结果列表a
return a
key = a[i] #设置列表的第1个元素作为关键数据
#print(key) #跟踪调试
while i < j: #循环直到i=j
while i < j and a[j] >= key: #j开始向前搜索,找到第一个小于key的值a[j]
j = j-1
a[i] = a[j]
while i < j and a[i] <= key: #i开始向后搜索,找到第一个大于key的a[i]
i = i+1
a[j] = a[i]
a[i] = key #a[i]等于关键数据
#print(a) #跟踪调试
quickSort(a, low, i-1) #递归调用快速排序算法(列表下界为low,上界为i-1)
quickSort(a, j+1, high) #递归调用快速排序算法(列表下界为j+1,上界为high)
def main():
a = [59,12,77,64,72,69,46,89,31,9]
quickSort(a, 0, len(a)-1)
print(a)
if __name__ == '__main__': main()
[9, 12, 31, 46, 59, 64, 69, 72, 77, 89]
11.3.6 Python提供的排序算法
在 Python 中,提供了两种排序方法:sort() 和 sorted()。
sort() 是列表对象的一个方法,用于原地排序,即对原列表进行排序操作。sort() 排序时,可以指定关键字参数 key,用于指定排序时使用的比较函数。
sorted() 是 Python 内置函数,用于对任意序列进行排序。sorted() 函数返回一个新的排好序的列表,不会改变原序列。sorted() 函数也可以指定关键字参数 key,用于指定排序时使用的比较函数。
这两种排序方法的时间复杂度都为 O(nlogn),其中 n 表示待排序序列的长度。在最坏情况下,需要进行 nlogn 次比较和移动,因此时间复杂度为 O(nlogn)。在最好情况下,待排序序列已经有序,只需要进行 n-1 次比较和 0 次移动,因此时间复杂度为 O(n)。
需要注意的是,sort() 方法是对原列表进行排序操作,因此会改变原列表,而 sorted() 函数返回一个新的排好序的列表,不会改变原序列。如果需要保留原序列,可以使用 sorted() 函数进行排序,或者使用 copy() 方法复制一份原列表进行排序。
11.4 常用数据结构
11.4.1 数据结构概述
数据结构包括数据的逻辑结构、物理结构和运算结构。
-
数据的逻辑结构:数据的逻辑结构指的是数据对象之间的逻辑关系,包括线性结构、树形结构、图形结构等。其中,线性结构包括线性表、栈、队列等,树形结构包括二叉树、B-树、堆等,图形结构包括有向图、无向图等。
-
数据的物理结构:数据的物理结构指的是数据在计算机内存中的存储形式,包括顺序存储结构和链式存储结构。其中,顺序存储结构是将数据按照顺序存储在一段连续的存储空间中,链式存储结构则是通过指针将数据存储在不连续的存储空间中。
-
数据的运算结构:数据的运算结构指的是对数据对象进行操作的运算,包括插入、删除、查找、排序等。不同的数据结构支持不同的运算结构,例如线性表支持插入、删除、查找等基本操作,而树和图支持更复杂的操作,如遍历、搜索等。
数据结构的逻辑结构决定了数据之间的关系和操作方式,物理结构决定了数据在计算机内存中的存储方式,运算结构则决定了数据的操作方式。掌握数据结构的基本概念和常用算法,对于提高程序员的编程能力和解决实际问题都有很大的帮助。
11.4.2 常用数据结构概述
常用的数据结构包括以下几种:
-
数组:数组是一种线性结构,用于存储同类型的数据元素,可以通过下标随机访问元素。数组的缺点是插入和删除元素时需要移动其他元素,效率较低。
-
链表:链表也是一种线性结构,通过指针将不连续的数据元素串联起来。链表的插入和删除操作效率较高,但随机访问元素的效率较低。
-
栈:栈是一种后进先出(LIFO)的数据结构,可以通过 push 和 pop 操作进行入栈和出栈。栈常用于表达式求值、递归函数调用等场景。
-
队列:队列是一种先进先出(FIFO)的数据结构,可以通过 enqueue 和 dequeue 操作进行入队和出队。队列常用于处理消息、任务等场景。
-
树:树是一种非线性结构,由节点和边组成,每个节点最多有一个父节点和多个子节点,根节点没有父节点。树的常用操作包括遍历、搜索和修改。
-
图:图是一种非线性结构,由节点和边组成,每个节点可以有多个父节点和多个子节点。图的常用操作包括遍历、搜索和最短路径等。
-
堆:堆是一种树形结构,分为最大堆和最小堆,最大堆中每个节点的值都大于或等于其子节点的值,最小堆中每个节点的值都小于或等于其子节点的值。堆的常用操作包括插入、删除和堆排序等。
-
散列表:散列表是一种通过散列函数将键映射到值的数据结构,支持快速的插入、删除和查找操作。散列表的缺点是空间利用率较低,散列函数设计不当会导致冲突。
这些数据结构都有各自的优缺点和适用场景,程序员需要根据实际问题选择合适的数据结构。掌握这些数据结构的基本概念和常用算法,对于提高程序员的编程能力和解决实际问题都有很大的帮助。
11.5 数组
Python没有提供直接数组数据类型,通常使用列表作为数组,或者使用标准库array模块中的array对象作为数组.如果要实现高效的数值计算,通常使用第三方科学计算模块NumPy来实现数组.
11.5.1 列表和数组
列表支持数组要求的4种核心操作,即创建数组,索引访问,索引赋值和迭代遍历
生成一副扑克牌,随机洗牌后输出一副扑克牌
import random
SUITS = ['Club', 'Diamond', 'Heart', 'Spade'] #梅花、方块、红桃、黑桃
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
#生成一副扑克牌,每副扑克牌包含52张牌(大小王除外)
deck = [] #一副扑克牌
for rank in RANKS:
for suit in SUITS:
card = rank + ' of ' + suit
deck += [card]
#洗牌
n = len(deck)
for i in range(n):
r = random.randrange(i, n)
temp = deck[r]
deck[r] = deck[i]
deck[i] = temp
#输出一副扑克牌
print(len(deck))
for s in deck: print(s)
11.5.2 array.array对象和数组
在 Python 中,提供了两种类型的数组:Python内置的数组和NumPy库中的数组。下面分别介绍这两种数组:
- Python内置数组:Python 的 array 模块提供了一个叫做 array 的类,用于创建一维数组。array 对象在创建时需要指定数据类型和数组元素,可以通过下标随机访问数组元素。array 对象的元素类型必须是固定的,不支持动态插入和删除元素,因此适合于只需要固定长度的数组场景。
下面是一个创建和使用 array 对象的示例:
import array
# 创建一个整型数组
arr = array.array('i', [1, 2, 3, 4, 5])
# 访问数组元素
print(arr[0]) # 输出 1
print(arr[2]) # 输出 3
# 修改数组元素
arr[1] = 10
# 遍历数组元素
for x in arr:
print(x)
- NumPy数组:NumPy 是 Python 中的一个科学计算库,提供了多维数组对象 ndarray,可以用于处理大规模数据和矩阵计算。NumPy 中的数组元素类型可以自由选择,支持动态插入和删除元素,提供了丰富的数组操作和数学函数。
下面是一个创建和使用 NumPy 数组的示例:
import numpy as np
# 创建一个一维数组
arr = np.array([1, 2, 3, 4, 5])
# 访问数组元素
print(arr[0]) # 输出 1
print(arr[2]) # 输出 3
# 修改数组元素
arr[1] = 10
# 遍历数组元素
for x in arr:
print(x)
总的来说,Python 内置的 array 对象适用于固定长度的数组场景,而 NumPy 数组适用于处理大规模数据和矩阵计算的场景。两种数组都支持随机访问元素,但 NumPy 数组提供了更多的数组操作和数学函数,适用于科学计算和数据分析。
11.6 栈和队列
先进先出
11.6.1 栈的实现:使用列表
列表可以实现队列(Queue),但并不适合.因为从列表的头部移除一个元素,列表的所有元素都需要移动位置,所以效率不高,此时可以使用collections模块种的deque对象.
列表实现栈示例:
class Stack:
def __init__(self,size = 16): #初始化栈
self.stack = []
def push(self,obj): #入栈操作(push)
self.stack.append(obj)
def pop(self): #出栈操作(pop)
try:
return self.stack.pop()
except IndexError as e:
print("stack is empty")
def __str__(self):
return str(self.stack)
def main():
stack = Stack() #创建并初始化栈
stack.push(1) #整数1入栈
stack.push(2) #整数2入栈
print(stack) #打印栈的内容
stack.pop() #整数2出栈
stack.pop() #整数1出栈
stack.pop() #出栈操作,但是因为是空栈,提示"stack is empty"
if __name__ == '__main__': main()
[1, 2] stack is empty
11.6.2 deque对象
在 Python 中,deque 是一种双端队列(double-ended queue),可以从两端添加或删除元素,它是 Python 标准库 collections 中的一个类。
deque 对象支持以下常用操作:
- append(x):在队列的右侧添加元素 x。
- appendleft(x):在队列的左侧添加元素 x。
- pop():从队列的右侧删除并返回最后一个元素。
- popleft():从队列的左侧删除并返回第一个元素。
- clear():清空队列中所有元素。
- count(x):返回队列中元素 x 的数量。
- extend(iterable):在队列的右侧添加可迭代对象 iterable 中的所有元素。
- extendleft(iterable):在队列的左侧添加可迭代对象 iterable 中的所有元素,注意添加顺序与 iterable 相反。
- index(x[, start[, stop]]):返回元素 x 在队列中第一次出现的位置,可选参数 start 和 stop 分别表示查找的起始位置和结束位置。
- remove(value):从队列中删除第一个值为 value 的元素。
- rotate(n=1):将队列向右旋转 n 步,n 为负数时向左旋转。
下面是一个使用 deque 的示例,
作为栈,deque对象方法append()用于入栈操作;pop()用于出栈操作.
作为队列,deque()对象方法append()用于进队操作;popleft()用于出队操作.
from collections import deque
# 创建一个空的双端队列
d = deque()
# 在队列的右侧添加元素
d.append(1)
d.append(2)
d.append(3)
# 在队列的左侧添加元素
d.appendleft(0)
# 获取队列的长度
print(len(d)) # 输出 4
# 从队列的右侧删除并返回最后一个元素
print(d.pop()) # 输出 3
# 从队列的左侧删除并返回第一个元素
print(d.popleft()) # 输出 0
# 将队列向右旋转一步
d.rotate()
# 遍历队列中的元素
for x in d:
print(x)
总的来说,deque 对象是一种功能强大的双端队列,支持从两端添加或删除元素,并提供了一系列常用操作,使用起来比普通列表更加方便。
11.7 集合
集合数据类型是没有顺序的简单对象的聚集,且集合中的元素不重复.Python集合数据类型包括可变集合对象(set)和不可变对象(frozenset).
11.7.1 集合的定义
在 Python 中,集合(set)是一种无序,不重复的数据集合,用于存储一组唯一的数据,它是 Python 的内置数据类型之一。集合中的元素必须是可哈希的,例如数字、字符串、元组等,集合本身是可变的,但集合中的元素必须是不可变的。
Python 中的集合可以使用 {} 或 set() 创建,下面是一些常用的创建集合的方法:
# 使用 {} 创建一个空集合
s = {}
# 创建一个有元素的集合
s = {1, 2, 3}
# 使用 set() 函数创建一个空集合
s = set()
# 使用 set() 函数创建一个有元素的集合
s = set([1, 2, 3])
需要注意的是,使用 {} 创建一个空集合时,会创建一个空字典,而不是一个空集合。因此,创建一个空集合时,应该使用 set() 函数。
集合支持以下常用操作:
- 添加元素:使用 add() 方法向集合中添加一个元素,使用 update() 方法向集合中添加多个元素。
- 删除元素:使用 remove() 或 discard() 方法删除集合中的元素。
- 检查元素:使用 in 或 not in 操作符检查集合中是否存在某个元素。
- 遍历元素:使用 for 循环遍历集合中的元素。
下面是一些使用集合的示例:
# 创建一个集合
s = {1, 2, 3}
# 添加一个元素
s.add(4)
# 添加多个元素
s.update([5, 6, 7])
# 删除一个元素
s.remove(7)
# 检查元素是否存在
print(2 in s) # 输出 True
print(7 in s) # 输出 False
# 遍历集合中的元素
for x in s:
print(x)
总的来说,集合是一种无序的、不重复的数据集合,用于存储一组唯一的数据。集合支持添加、删除、检查和遍历操作,使用起来比列表更加高效,适用于需要存储一组唯一数据的场景。
11.7.2 集合解析表达式
集合解析表达式(Set comprehension)是一种用于创建集合的简洁语法,类似于列表解析表达式和字典解析表达式。使用集合解析表达式可以快速创建一个集合,语法如下:
{expression for item in iterable if condition}
其中,expression 是一个表达式,用于计算集合中的元素;item 是 iterable 中的元素;condition 是一个可选的条件表达式,用于过滤 iterable 中的元素。
下面是一些使用集合解析表达式的示例:
# 创建一个集合,包含 1~10 中的偶数
s = {x for x in range(1, 11) if x % 2 == 0}
print(s) # 输出 {2, 4, 6, 8, 10}
# 创建一个集合,包含两个列表的交集
a = [1, 2, 3, 4]
b = [3, 4, 5, 6]
s = {x for x in a if x in b}
print(s) # 输出 {3, 4}
# 创建一个集合,包含字符串中的所有元音字母
s = "hello world"
vowels = {'a', 'e', 'i', 'o', 'u'}
result = {x for x in s if x in vowels}
print(result) # 输出 {'e', 'o'}
总的来说,集合解析表达式是一种简洁的语法,用于快速创建集合。它的语法类似于列表解析表达式和字典解析表达式,可以使用条件表达式过滤 iterable 中的元素。使用集合解析表达式可以使代码更加简洁和易读。
11.7.3 判断集合元素是否存在
x in s
s not in s
11.7.4 集合的运算:并集,交集,差集和对称差集
Python 中的集合支持一系列集合运算,包括交集、并集、差集和对称差集等。下面是一个包括运算符和说明的表格:
运算符 | 说明 | 示例 |
---|---|---|
| | 取两个集合的并集 | {1, 2, 3} | {3, 4, 5} 等于 {1, 2, 3, 4, 5} |
& | 取两个集合的交集 | {1, 2, 3} & {3, 4, 5} 等于 {3} |
- | 取两个集合的差集 | {1, 2, 3} - {3, 4, 5} 等于 {1, 2} |
^ | 取两个集合的对称差集 | {1, 2, 3} ^ {3, 4, 5} 等于 {1, 2, 4, 5} |
上面的示例中,{1, 2, 3}
和 {3, 4, 5}
是两个集合,用运算符对它们进行集合运算后得到了新的集合。其中,|
表示并集,&
表示交集,-
表示差集,^
表示对称差集。这些运算符可以用于集合之间的操作,返回新的集合对象,但不会修改原有集合对象。
总的来说,Python 中的集合支持丰富的集合运算,可以用运算符进行集合的交、并、差、对称差等操作,方便进行集合计算和数据处理。
集合的对象方法
下面是一个包括集合对象方法的表格:
方法 | 说明 |
---|---|
add(elem) | 向集合中添加一个元素 |
clear() | 清空集合中的所有元素 |
copy() | 返回一个集合的浅拷贝 |
difference(*others) 或 difference_update(*others) | 返回一个包含集合和其他集合中所有不重复元素的新集合,或者直接修改集合本身,并将结果保存在集合中 |
discard(elem) 或 remove(elem) | 从集合中删除指定元素 |
intersection(*others) 或 intersection_update(*others) | 返回一个包含集合和其他集合中所有共同元素的新集合,或者直接修改集合本身,并将结果保存在集合中 |
isdisjoint(other) | 判断集合与其他集合是否没有共同元素 |
issubset(other) 或 issuperset(other) | 判断集合是否是其他集合的子集或超集 |
pop() | 删除并返回集合中的任意一个元素 |
symmetric_difference(other) 或 symmetric_difference_update(other) | 返回一个包含集合和其他集合中不重复元素的新集合,或者直接修改集合本身,并将结果保存在集合中 |
union(*others) | 返回一个包含集合和其他集合中所有元素的新集合 |
update(*others) | 把其他集合中的元素添加到集合中 |
其中,add() 方法用于向集合中添加一个元素;clear() 方法用于清空集合中的所有元素;copy() 方法返回一个集合的浅拷贝;difference() 方法返回一个包含集合和其他集合中所有不重复元素的新集合,或者直接修改集合本身,并将结果保存在集合中;discard() 和 remove() 方法用于从集合中删除指定元素;intersection() 方法返回一个包含集合和其他集合中所有共同元素的新集合,或者直接修改集合本身,并将结果保存在集合中;isdisjoint() 方法用于判断集合与其他集合是否没有共同元素;issubset() 和 issuperset() 方法用于判断集合是否是其他集合的子集或超集;pop() 方法用于删除并返回集合中的任意一个元素;symmetric_difference() 方法返回一个包含集合和其他集合中不重复元素的新集合,或者直接修改集合本身,并将结果保存在集合中;union() 方法返回一个包含集合和其他集合中所有元素的新集合;update() 方法把其他集合中的元素添加到集合中。
11.7.5 集合的比较运算:相等,子集和超集
另外,<=
表示子集,<
表示真子集,>=
表示超集,>
表示真超集。
<= | 判断一个集合是否是另一个集合的子集 | {1, 2} <= {1, 2, 3} 等于 True |
---|---|---|
< | 判断一个集合是否是另一个集合的真子集 | {1, 2} < {1, 2, 3} 等于 True |
>= | 判断一个集合是否是另一个集合的超集 | {1, 2, 3} >= {1, 2} 等于 True |
> | 判断一个集合是否是另一个集合的真超集 | {1, 2, 3} > {1, 2} 等于 True |
11.7.6 集合的长度,最大值,最小值,元素和
在 Python 中,集合(set)是一种无序不重复元素的集合数据类型,因此它不支持索引、切片、排序等操作。但是,我们可以使用内置函数和方法对集合进行常用操作,包括计算集合的长度、最大值、最小值和元素和等。下面是一些常用的集合操作:
- len(set):计算集合中元素的个数,即集合的长度。
- max(set):返回集合中最大的元素,如果集合为空,则会抛出 ValueError 异常。
- min(set):返回集合中最小的元素,如果集合为空,则会抛出 ValueError 异常。
- sum(set):返回集合中所有元素的和,如果集合为空,则返回 0。
下面是一些使用集合操作的示例:
# 创建一个集合
s = {1, 2, 3, 4, 5}
# 计算集合的长度
print(len(s)) # 输出 5
# 计算集合中的最大值和最小值
print(max(s)) # 输出 5
print(min(s)) # 输出 1
# 计算集合中所有元素的和
print(sum(s)) # 输出 15
总的来说,集合数据类型支持一些常用的操作,包括计算集合的长度、最大值、最小值和元素和等。这些操作可以使用内置函数和方法来实现,方便对集合进行处理和操作。需要注意的是,由于集合是无序的,因此它不支持索引、切片、排序等操作。
11.7.7 可变集合的方法
在 Python 中,集合分为可变集合(set)和不可变集合(frozenset)。可变集合是一种可以进行增删改操作的集合类型,它支持一系列常用的方法,用于操作集合对象。下面是一些常用的可变集合方法:
方法 | 说明 | 示例 |
---|---|---|
add(elem) | 向集合中添加一个元素 | s = {1, 2, 3} s.add(4) print(s) 输出 {1, 2, 3, 4} |
clear() | 清空集合中的所有元素 | s = {1, 2, 3} s.clear() print(s) 输出 set() |
copy() | 返回一个集合的浅拷贝 | s1 = {1, 2, 3} s2 = s1.copy() print(s2) 输出 {1, 2, 3} |
difference(other) | 返回一个包含集合和其他集合中所有不重复元素的新集合 | s1 = {1, 2, 3} s2 = {3, 4, 5} s3 = s1.difference(s2) print(s3) 输出 {1, 2} |
difference_update(other) | 直接修改集合本身,并将结果保存在集合中 | s1 = {1, 2, 3} s2 = {3, 4, 5} s1.difference_update(s2) print(s1) 输出 {1, 2} |
discard(elem) | 从集合中删除指定元素,如果元素不存在,则不进行任何操作 | s = {1, 2, 3} s.discard(2) print(s) 输出 {1, 3} |
intersection(other) | 返回一个包含集合和其他集合中所有共同元素的新集合 | s1 = {1, 2, 3} s2 = {3, 4, 5} s3 = s1.intersection(s2) print(s3) 输出 {3} |
intersection_update(other) | 直接修改集合本身,并将结果保存在集合中 | s1 = {1, 2, 3} s2 = {3, 4, 5} s1.intersection_update(s2) print(s1) 输出 {3} |
isdisjoint(other) | 判断集合与其他集合是否没有共同元素,如果没有,则返回 True,否则返回 False | s1 = {1, 2, 3} s2 = {4, 5, 6} print(s1.isdisjoint(s2)) 输出 True |
issubset(other) | 判断集合是否是其他集合的子集,如果是,则返回 True,否则返回 False | s1 = {1, 2} s2 = {1, 2, 3} print(s1.issubset(s2)) 输出 True |
issuperset(other) | 判断集合是否是其他集合的超集,如果是,则返回 True,否则返回 False | s1 = {1, 2, 3} s2 = {1, 2} print(s1.issuperset(s2)) 输出 True |
pop() | 删除并返回集合中的任意一个元素,如果集合为空,则抛出 KeyError 异常 | s = {1, 2, 3} print(s.pop()) 输出 1 |
remove(elem) | 从集合中删除指定元素,如果元素不存在,则抛出 KeyError 异常 | s = {1, 2, 3} s.remove(2) print(s) 输出 {1, 3} |
symmetric_difference(other) | 返回一个包含集合和其他集合中不重复元素的新集合 | s1 = {1, 2, 3} s2 = {3, 4, 5} s3 = s1.symmetric_difference(s2) print(s3) 输出 {1, 2, 4, 5} |
symmetric_difference_update(other) | 直接修改集合本身,并将结果保存在集合中 | s1 = {1, 2, 3} s2 = {3, 4, 5} s1.symmetric_difference_update(s2) print(s1) 输出 {1, 2, 4, 5,} |
union(*others) | 返回一个包含集合和其他集合中所有元素的新集合 | s1 = {1, 2, 3} s2 = {3, 4, 5} s3 = s1.union(s2) print(s3) 输出 {1, 2, 3, 4, 5} |
update(*others) | 把其他集合中的元素添加到集合中 | s1 = {1, 2, 3} s2 = {3, 4, 5} s1.update(s2) print(s1) 输出 {1, 2, 3, 4, 5} |
需要注意的是,这些方法都是可变集合方法,即它们会直接修改集合本身,并将结果保存在集合中。如果需要创建一个新的集合并保留原集合,可以使用集合的 copy() 方法来创建一个浅拷贝。
11.8 字典
字典(dict,或映射(map))是一组键/值对的数据结构.每个键对应一个值.在字典中键不能重复,根据键可以查询到值.
11.8.1 对象的hash值
在 Python 中,每个对象都有一个 hash 值,用于快速比较对象是否相等。hash 值是一个整数,可以通过内置函数 hash()
来获取。不同的对象的 hash 值一般是不同的,但是有些对象是可哈希的(hashable),它们的 hash 值在其生命周期内是不变的,这些对象包括数字、字符串、元组等不可变对象,而列表、字典等可变对象是不可哈希的。
当我们使用一个可哈希的对象作为字典的键或集合的元素时,Python 会自动计算该对象的 hash 值,并将其用作哈希表的索引。这样可以快速地定位到该对象所在的位置,提高字典和集合的查找效率。
下面是一些使用 hash()
函数获取对象 hash 值的示例:
# 获取整数的 hash 值
a = 123
print(hash(a)) # 输出 123
# 获取字符串的 hash 值
s = 'hello'
print(hash(s)) # 输出 3067076204635381907
# 获取元组的 hash 值
t = (1, 2, 3)
print(hash(t)) # 输出 2528502973977326415
# 列表是不可哈希的,会引发 TypeError 异常
lst = [1, 2, 3]
print(hash(lst)) # 抛出 TypeError 异常:unhashable type: 'list'
需要注意的是,Python 中的 hash 值是基于对象的内容计算出来的,因此对于相同内容的对象,它们的 hash 值应该是相同的。如果我们自定义一个类,可以通过重载 __hash__()
方法来定义对象的 hash 值。同时也要重载 __eq__()
方法,以确保相等的对象具有相同的 hash 值,否则在使用自定义类的对象作为字典键或集合元素时会出现意外的结果。
需要注意的是,在 Python 中,hash()
函数并不是百分之百安全的,因为它有可能会发生哈希冲突。哈希冲突是指两个不同的对象计算出来的 hash 值相同,这种情况下 Python 会在哈希表中使用链表来存储这些键值对,这会降低字典和集合的查找效率。因此,在使用自定义类的对象作为字典键或集合元素时,需要谨慎选择哈希函数,以尽可能避免哈希冲突。
11.8.2 字典的定义
在 Python 中,字典是一种无序的、可变的数据类型,它以键值对(key-value pairs)的形式存储数据。字典中的键(key)必须是不可变的数据类型,如字符串、数字或元组等不可变对象,而值(value)则可以是任意类型的数据,包括字符串、数字、列表、字典等任意对象。
字典是用花括号 {}
来定义的,每个键值对用冒号 :
分隔,多个键值对之间用逗号 ,
分隔。下面是一个简单的字典定义示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 打印字典
print(person)
输出结果为:
{'name': 'Tom', 'age': 25, 'gender': 'male'}
上述代码定义了一个名为 person
的字典,其中包含了三个键值对。键 'name'
对应的值为 'Tom'
,键 'age'
对应的值为 25
,键 'gender'
对应的值为 'male'
。可以看到,字典中的键值对是无序的,即键值对的顺序不一定与定义时的顺序相同。
我们可以通过键来访问字典中的值,方法是使用中括号 []
,并将键作为索引。下面是一个访问字典值的示例:
# 访问字典中的值
print(person['name']) # 输出 'Tom'
print(person['age']) # 输出 25
print(person['gender']) # 输出 'male'
# 如果访问不存在的键,会引发 KeyError 异常
print(person['address']) # 抛出 KeyError 异常:'address'
需要注意的是,如果访问字典中不存在的键,会引发 KeyError 异常。可以使用 in
关键字来检查键是否存在于字典中,或者使用 get()
方法来获取键对应的值,如果键不存在,则返回指定的默认值或者 None
。下面是一个检查键是否存在的示例:
# 判断键是否存在
if 'name' in person:
print('name is in person')
if 'address' not in person:
print('address is not in person')
# 使用 get() 方法获取键对应的值
print(person.get('name')) # 输出 'Tom'
print(person.get('address')) # 输出 None
print(person.get('address', 'N/A')) # 输出 'N/A'
除了以上介绍的常见操作之外,字典还支持一系列其他的方法,如增加、删除、修改键值对等。可以参考 Python 官方文档中的字典操作文档详细了解字典的操作方法。
11.8.3 字典的访问操作
在 Python 中,可以使用中括号 []
运算符来访问字典中的值,也可以使用 get()
方法来获取指定键对应的值。如果访问的键不存在,则使用中括号运算符会引发 KeyError 异常,而使用 get()
方法则会返回指定的默认值或者 None
。下面是一些访问字典的示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 使用中括号运算符访问字典
print(person['name']) # 输出 'Tom'
print(person['age']) # 输出 25
print(person['address']) # 抛出 KeyError 异常
# 使用 get() 方法访问字典
print(person.get('name')) # 输出 'Tom'
print(person.get('address')) # 输出 None
print(person.get('address', 'N/A')) # 输出 'N/A'
除了访问字典中的值之外,还可以使用中括号运算符来设置字典中的值。如果指定的键不存在,则会添加一个新的键值对;如果指定的键已经存在,则会更新该键对应的值。下面是一个设置字典中的值的示例:
# 设置字典中的值
person['name'] = 'Jerry' # 更新键 'name' 对应的值
person['address'] = '123 Main St' # 添加新键值对
print(person) # 输出 {'name': 'Jerry', 'age': 25, 'gender': 'male', 'address': '123 Main St'}
11.8.4 字典的视图对象
在 Python 中,字典有三种视图对象(view objects),分别是键视图对象(keys view)、值视图对象(values view)和项视图对象(items view)。这些视图对象可以用于遍历字典中的键、值或键值对。视图对象是动态的,即它们会反映字典中的实时变化,因此在遍历字典时最好使用视图对象来获取数据。下面是一些使用字典视图对象的示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 获取键视图对象
keys = person.keys()
print(keys) # 输出 dict_keys(['name', 'age', 'gender'])
# 获取值视图对象
values = person.values()
print(values) # 输出 dict_values(['Tom', 25, 'male'])
# 获取项视图对象
items = person.items()
print(items) # 输出 dict_items([('name', 'Tom'), ('age', 25), ('gender', 'male')])
# 遍历键视图对象
for key in keys:
print(key)
# 遍历值视图对象
for value in values:
print(value)
# 遍历项视图对象
for key, value in items:
print(key, value)
需要注意的是,视图对象不支持索引操作,因此不能使用中括号运算符来访问其中的元素。
11.8.5 字典的遍历
在 Python 中,可以使用循环语句来遍历字典中的键、值或键值对。可以使用 keys()
、values()
或 items()
方法来获取字典的视图对象,然后通过视图对象来遍历字典。下面是一些遍历字典的示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 遍历字典的键
for key in person.keys():
print(key)
# 遍历字典的值
for value in person.values():
print(value)
# 遍历字典的键值对
for key, value in person.items():
print(key, value)
需要注意的是,字典是无序的,因此在遍历字典时不能保证元素的顺序
11.8.6 字典解析表达式
在 Python 中,可以使用字典解析表达式(dictionary comprehension)来快速创建字典。字典解析表达式与列表解析表达式类似,可以通过一行代码来生成一个字典。下面是一个使用字典解析表达式创建字典的示例:
# 创建一个字典,键为 0 到 4 的整数,值为该键的平方
squares = {x: x**2 for x in range(5)}
print(squares) # 输出 {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
在字典解析表达式中,需要使用大括号 {}
来表示生成的字典,然后在大括号中使用类似于键值对的表达式来指定字典的键和值。字典解析表达式中的表达式可以是任意的 Python 表达式,可以利用循环、条件语句等来生成键值对。
11.8.7 判断字典键是否存在
在 Python 中,可以使用 in
关键字来判断一个键是否存在于字典中。如果键存在,则返回 True
,否则返回 False
。下面是一个示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 判断键是否存在
if 'name' in person:
print('name is in person')
if 'address' not in person:
print('address is not in person')
需要注意的是,在使用中括号运算符访问字典时,如果指定的键不存在,则会引发 KeyError 异常。因此在判断键是否存在时,最好使用 in
关键字,或者使用 get()
方法来获取键对应的值。
11.8.8 字典对象的长度和比较
在 Python 中,可以使用内置函数 len()
来获取字典中键值对的个数。下面是一个示例:
# 定义一个字典
person = {'name': 'Tom', 'age': 25, 'gender': 'male'}
# 获取字典的长度
print(len(person)) # 输出 3
除了获取字典的长度之外,还可以使用比较运算符来比较两个字典的大小,比较的方式是按照字典的键值对进行比较。下面是一个比较字典大小的示例:
# 定义两个字典
person1 = {'name': 'Tom', 'age': 25, 'gender': 'male'}
person2 = {'name': 'Jerry', 'age': 30, 'gender': 'male'}
# 比较字典的大小
print(person1 == person2) # 输出 False
print(person1 != person2) # 输出 True
print(person1 < person2) # 抛出 TypeError 异常
需要注意的是,字典不支持 <
、<=
、>
、>=
这些比较运算符,如果使用这些运算符进行字典的大小比较,会引发 TypeError 异常。
11.8.9 字典对象的方法
在 Python 中,字典对象有很多有用的方法,可以用于增加、删除、修改、查找、遍历字典等操作。下面是一些常用的字典方法:
-
dict.clear()
:清空字典中的所有键值对。 -
dict.copy()
:返回字典的一个浅拷贝。 -
dict.fromkeys(seq[, value])
:创建一个新字典,其中包含指定序列中的元素作为键,值都设置为指定的值。如果没有指定值,则默认为None
。 -
dict.get(key[, default])
:返回指定键对应的值,如果键不存在,则返回指定的默认值或者None
。 -
dict.items()
:返回一个包含所有 -
dict.items()
:返回一个包含所有键值对的元组的列表。 -
dict.keys()
:返回一个包含所有键的列表。 -
dict.values()
:返回一个包含所有值的列表。 -
dict.pop(key[, default])
:删除并返回指定键对应的值。如果键不存在,且没有指定默认值,则引发 KeyError 异常。 -
dict.popitem()
:随机删除并返回一个键值对。如果字典为空,则引发 KeyError 异常。 -
dict.setdefault(key[, default])
:返回指定键对应的值。如果键不存在,则插入一个新的键值对,值为指定的默认值或者None
。 -
dict.update([other])
:更新字典,将指定字典中的键值对添加到当前字典中。如果指定键已经存在,则更新该键对应的值。 -
dict.fromkeys(seq[, value])
:创建一个新字典,其中包含指定序列中的元素作为键,值都设置为指定的值。如果没有指定值,则默认为None
。
11.8.10 defaultdict对象
在 Python 中,defaultdict
是一个与字典类似的容器,它在创建时指定一个默认值,当访问不存在的键时,会自动返回默认值而不是引发 KeyError 异常。defaultdict
是使用 collections
模块提供的类。下面是一个使用 defaultdict
的示例:
from collections import defaultdict
# 创建一个 defaultdict,指定默认值为 0
d = defaultdict(int)
# 访问不存在的键,返回默认值 0
print(d['x']) # 输出 0
# 使用普通字典的方法添加键值对
d['a'] = 1
d['b'] = 2
d['c'] = 3
print(d) # 输出 defaultdict(<class 'int'>, {'a': 1, 'b': 2, 'c': 3})
在上面的示例中,使用 defaultdict
创建了一个默认值为 0
的字典 d
,当访问不存在的键 'x'
时,返回默认值 0
。然后使用普通字典的方法向字典中添加键值对。可以看到,当访问存在的键时,返回对应的值;当访问不存在的键时,返回默认值。
11.8.11 OrderedDict对象
在 Python 中,OrderedDict
是一个有序字典,它会按照元素添加的顺序来保持键的顺序。OrderedDict
是使用 collections
模块提供的类。下面是一个使用 OrderedDict
的示例:
from collections import OrderedDict
# 创建一个有序字典
d = OrderedDict()
# 向有序字典中添加键值对
d['a'] = 1
d['b'] = 2
d['c'] = 3
print(d) # 输出 OrderedDict([('a', 1), ('b', 2), ('c', 3)])
# 遍历有序字典
for key, value in d.items():
print(key, value)
在上面的示例中,使用 OrderedDict
创建了一个有序字典 d
,然后向其中添加了三个键值对。可以看到,当遍历字典时,输出的键值对顺序与添加的顺序一致。
11.8.12 ChainMap对象
在 Python 中,ChainMap
是一个将多个字典或映射对象(如 dict
、OrderedDict
、defaultdict
等)合并成一个字典的容器。当访问键时,ChainMap
会按照添加的顺序依次在各个字典中查找。ChainMap
是使用 collections
模块提供的类。下面是一个使用 ChainMap
的示例:
from collections import ChainMap
# 创建两个字典
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c':4}
# 创建 ChainMap
cm = ChainMap(dict1, dict2)
# 访问键
print(cm['a']) # 输出 1
print(cm['b']) # 输出 2,因为在 dict1 中找到了键 b
print(cm['c']) # 输出 4,因为在 dict2 中找到了键 c
在上面的示例中,使用 ChainMap
创建了一个容纳了两个字典 dict1
和 dict2
的 cm
。然后访问了三个键,可以看到,当键在第一个字典 dict1
中找到时,返回对应的值;否则继续在第二个字典 dict2
中查找,直到找到为止。
11.8.13 Counter对象
在 Python 中,Counter
是一个简单的计数器,用于统计元素出现的次数,并以字典的形式返回统计结果。Counter
是使用 collections
模块提供的类。下面是一个使用 Counter
的示例:
from collections import Counter
# 创建一个计数器
c = Counter('abbcccddddeeeee')
# 打印计数器
print(c) # 输出 Counter({'e': 5, 'd': 4, 'c': 3, 'b': 2, 'a': 1})
# 访问计数器中的元素
print(c['d']) # 输出 4
print(c['f']) # 输出 0,因为 f 没有出现过
在上面的示例中,使用 Counter
创建了一个计数器 c
,统计了字符串 'abbcccddddeeeee'
中每个字符出现的次数,然后打印了计数器。可以看到,计数器以字典的形式返回了统计结果。然后访问了两个元素,可以看到,当元素不存在时,返回值为 0
。
11.9 collections模块的其他数据结构
11.9.1 namedtuple对象
namedtuple
是 Python 中的一个工厂函数,用于创建一个具有命名字段的元组子类,它可以像普通元组一样访问元素,也可以像普通类一样访问字段。namedtuple
是使用 collections
模块提供的类。下面是一个使用 namedtuple
的示例:
from collections import namedtuple
# 创建一个具有命名字段的元组子类
Point = namedtuple('Point', ['x', 'y'])
# 创建一个 Point 对象
p = Point(1, 2)
# 访问元素和字段
print(p[0], p[1]) # 输出 1 2
print(p.x, p.y) # 输出 1 2
在上面的示例中,使用 namedtuple
创建了一个名为 Point
的元组子类,它具有两个命名字段 x
和 y
。然后创建了一个 Point
对象 p
,并访问了元素和字段,可以看到,它们都可以正常访问。
11.9.2 UserDict、UserList和UserString对象
UserDict
、UserList
和 UserString
是 Python 中的三个内置容器类,它们分别继承了 dict
、list
和 str
类,可以用于创建用户自定义的字典、列表和字符串。这三个类的作用是让用户可以更方便地自定义这些容器的行为,例如修改添加元素的方式、修改元素的访问方式等。下面是一个使用 UserDict
的示例:
from collections import UserDict
# 创建一个用户自定义的字典
class MyDict(UserDict):
def __init__(self):
super().__init__()
self['default'] = 0
def __missing__(self, key):
return self['default']
# 创建一个 MyDict 对象
d = MyDict()
# 向 MyDict 中添加键值对
d['a'] = 1
d['b'] = 2
print(d) # 输出 {'a': 1, 'b': 2, 'default': 0}
# 访问 MyDict 中的元素
print(d['a']) # 输出 1
print(d['c']) # 输出 0,因为键 'c' 不存在
在上面的示例中,创建了一个名为 MyDict
的用户自定义字典类,它继承了 UserDict
类,并重写了 __missing__
方法,用于处理访问不存在的键时的行为。然后创建了一个 MyDict
对象 d
,并向其中添加了两个键值对。可以看到,当访问不存在的键时,返回的是默认值 0
。
11.10 应用举例
用户可以构造一个集合来去除列表中的重复项,但结果不能保证原来的顺序.
def unique(items):
items_existed = set()
for item in items:
if item not in items_existed:
yield item
items_existed.add(item)
if __name__ == "__main__":
#测试代码
a = [1, 8, 5, 1, 9, 2, 1, 10]
a1 = unique(a)
print(list(a1))
[1, 8, 5, 9, 2, 10]
11.10.2 基于字典的通讯录
设计一个简单的基于字典数据结构的通讯录管理系统,该系统采用Json文件来保存数据.通讯录设计为字典{name:tel}.程序开始时从addressbook.json文件中读取通讯录,然后显示主菜单,具体包括如下功能
- 显示通讯录清单
- 查询联系人资料
- 插入新的联系人
- 删除已有的联系人
- 退出
[addressbook.json](D:\jupyter\python基础\Untitled Folder\addressbook.json)
"""简易通信录程序"""
import os, json
ab = {} #通信录保存在字典中name:tel
#从JSON文件中读取通信录
if os.path.exists("addressbook.json"):
with open(r'addressbook.json', 'r', encoding='utf-8') as f:
ab = json.load(f)
while True:
print("|---欢迎使用通讯录程序---|")
print("|---1:显示通讯录清单 ---|")
print("|---2:查询联系人资料 ---|")
print("|---3:插入新的联系人 ---|")
print("|---4:删除已有联系人 ---|")
print("|---0:退出 -------------|")
choice = input('请选择功能菜单(0-3):')
if choice == '1':
if(len(ab)==0):
print("通讯录为空")
else:
for k, v in ab.items():
print("姓名={},联系电话={}".format(k, v))
elif choice == '2':
name = input("请输入联系人姓名:")
if(name not in ab):
ask = input("联系人不存在,是否增加用户资料(Y/N)")
if ask in ["Y", "y"]:
tel = input("请输入用户联系电话:")
ab[name] = tel
else:
print("联系人信息:{} {}".format(name, ab[name]))
elif choice == '3':
name=input("请输入联系人姓名:")
if(name in ab):
print("已存在联系人:{} {}".format(name, ab[name]))
ask = input("是否修改用户资料(Y/N)")
if ask in ["Y", "y"]:
tel = input("请输入用户联系电话:")
dict[name] = tel
else:
tel = input("请输入用户联系电话:")
ab[name] = tel
elif choice == '4':
name = input("请输入联系人姓名:")
if(name not in ab):
print("联系人不存在:{}".format(name))
else:
tel = ab.pop(name)
print("删除联系人:{} {}".format(name, tel))
elif choice == '0': #保存到JSON文件并退出循环
with open(r'addressbook.json', 'w', encoding='utf-8') as f:
json.dump(ab, f)
break
print("|---4:删除已有联系人 ---|")
print("|---0:退出 -------------|")
choice = input('请选择功能菜单(0-3):')
if choice == '1':
if(len(ab)==0):
print("通讯录为空")
else:
for k, v in ab.items():
print("姓名={},联系电话={}".format(k, v))
elif choice == '2':
name = input("请输入联系人姓名:")
if(name not in ab):
ask = input("联系人不存在,是否增加用户资料(Y/N)")
if ask in ["Y", "y"]:
tel = input("请输入用户联系电话:")
ab[name] = tel
else:
print("联系人信息:{} {}".format(name, ab[name]))
elif choice == '3':
name=input("请输入联系人姓名:")
if(name in ab):
print("已存在联系人:{} {}".format(name, ab[name]))
ask = input("是否修改用户资料(Y/N)")
if ask in ["Y", "y"]:
tel = input("请输入用户联系电话:")
dict[name] = tel
else:
tel = input("请输入用户联系电话:")
ab[name] = tel
elif choice == '4':
name = input("请输入联系人姓名:")
if(name not in ab):
print("联系人不存在:{}".format(name))
else:
tel = ab.pop(name)
print("删除联系人:{} {}".format(name, tel))
elif choice == '0': #保存到JSON文件并退出循环
with open(r'addressbook.json', 'w', encoding='utf-8') as f:
json.dump(ab, f)
break