master
/ 7.1.1 集合的创建.ipynb

7.1.1 集合的创建.ipynb @masterview markup · raw · history · blame

Notebook

集合(set)

集合类型用来保存**无序**的、**不重复**不可变数据</font>,其概念和数学中集合的概念基本一致,如果我们把一定范围的、确定的、可以区别的事物当作一个整体来看待,那么这个整体就是集合,集合中的各个事物称为集合的元素。通常,集合需要满足以下特性:

1.无序性:一个集合中,每个元素的地位都是相同的,元素之间是无序的。 2.互异性:一个集合中,任何两个元素都是不相同的,即元素在集合中只能出现一次,集合中不存在重复的元素。 3.确定性:给定一个集合和一个任意元素,该元素要么属这个集合,要么不属于这个集合,二者必居其一,不允许有模棱两可的情况出现。

In [2]:
set1 = {'Python', 'C++', 'Java', 'Kotlin', 'Swift'}  # 集合

for elem in set1:         # 遍历集合中的元素
    print(elem,end=' ')

# 每次运行结果可能不同
# C++ Swift Python Kotlin Java
# Java Kotlin C++ Swift Python 
Kotlin Java C++ Swift Python 

无序性说明集合中的元素并不像列中的元素那样存在某种次序,可以通过索引运算就能访问任意元素,集合并不支持索引运算。
集合的互异性决定了集合中不能有重复元素,这一点也是集合区别于列表的地方,我们无法将重复的元素添加到一个集合中。集合类型必然是支持in和not in成员运算的,这样就可以确定一个元素是否属于集合,也就是上面所说的集合的确定性。
集合的成员运算在性能上要优于列表的成员运算,这是集合的底层存储特性决定的(集合底层使用了哈希存储(散列存储))

可近似将集合看成是没有值的字典
常见的用途:
1.成员检测 2.从序列中去除重复项 3.数学中的集合类计算

目前有两种内置集合类型:
**集合(set)**
**不可变集合(frozenset)**

set和frozenset的关系类似于列表和元组的关系。
set 类型是可变但 unhashable的,其内容可以使用 add() 和 remove() 这样的方法来改变, 但不能被用作字典的键或其他集合的元素;
frozenset 类型是不可变并且为 hashable,因此它可以被用作字典的键或其他集合的元素。

1. 集合的创建

1.1 非空集合可通过将一系列用逗号分隔的数据放在一对**大括号**中的方法创建。

In [11]:
setA = {1, 2, 3, 4, 5}  # 将集合数据赋值给变量,直接创建集合
print(setA)             # 输出集合{1, 2, 3, 4, 5}
setB = {'吉林', '武汉', '北京'}
print(setB)             # {'北京', '吉林', '武汉'}
setC = {(1, 2, 3,3), 1, 2, 3.14, 'hello',(1, 2, 3,3)}
print(setC)             # {(1, 2, 3), 1, 2, 3.14, 'hello'}
{1, 2, 3, 4, 5}
{'北京', '武汉', '吉林'}
{1, 2, 3.14, (1, 2, 3, 3), 'hello'}

需要注意的是,。{}中需要至少有一个元素,不能直接使用**“{}”**来创建和表示**空集合**,“{}”将创建一个**空字典**

In [13]:
setD = {1}    # 将创建一个空字典
print(setD)  
print(type(setD)) # 输出类型为dict
{1}
<class 'set'>

集合元素必须为不可变类型**</font>,如“{}”内包含可变类型,将会抛TypeError异常。

In [ ]:
setC = {[1, 2, 3], 1, 2, 3.14, 'hello'} # 列表[1,2,3]为可变类型,抛TypeError异常

由于集合元素**不重复**,如包含重复元素,将会**去重**

In [12]:
set_1 = {(1, 2, 3), 1, 1, 1, 1, 1, 2, 3.14, 'hello'}  # 包含多个整数1,将会去重,最终字典中将只包含一个1
print(set_1)    # {1, 2, 3.14, 'hello', (1, 2, 3)}
set_2 = {0, 0.0 ,0j}    # 由于数值上 0 == 0.0 == 0j,所以同样会去重
print(set_2)   # {0}
{1, 2, 3.14, (1, 2, 3), 'hello'}
{0}

1.2 使用 set() 函数可创建集合(set)

set()和 frozenset()函数又称为集合构造器,分别用来生成可变和不可变的集合。
如果提供一个参数,则该**参数**必须是可迭代的,即参数必须是序列、列表、元组、推导式、迭代器或字典等支持**迭代的**对象。

In [15]:
setE = set(range(8))            # 通过range创建集合 {0, 1, 2, 3, 4, 5, 6, 7}并赋值给 S5
print(setE)                     # 输出{0, 1, 2, 3, 4, 5, 6, 7}

setF = set([1, 2, 3, 4, 5])     # set()将列表转为集合 {1, 2, 3, 4, 5}

print(setF)                     # 输出{1, 2, 3, 4, 5}

print(set('cheeseshop'))        # 字符串转为集合,去掉重复元素, {'c', 'p', 'o', 'e', 's', 'h'}
{0, 1, 2, 3, 4, 5, 6, 7}
{1, 2, 3, 4, 5}
{'p', 's', 'h', 'e', 'o', 'c'}

1.3 使用 frozenset() 函数可创建不可变集合(frozenset)

In [ ]:
setG = frozenset((1, 3, 5, 7))  # 用frozenset()函数将元组转为不可变集合
print(setG)                     # 输出 frozenset({1, 3, 5, 7})

如果不提供任何参数,默认会生成**空集合**

In [ ]:
setI = set()             # 使用函数创建一个空集合,空集合不能使用{}创建和表示
print(setI)              # 输出 set()
setJ = frozenset()       # 使用 frozenset()函数构造器创建一个空的不可变集合
print(setJ)              # 输出 frozenset()

1.4 利用集合推导式

与列表推导式类似,集合也可以通过集合推导式创建。
集合推导式与列表推导式的唯一区别在于用**{}**代替[]。

In [16]:
setH = {i*i for i in range(5)}  # 利用推导式生成集合
print(setH)                     # {0, 1, 4, 9, 16}
{0, 1, 4, 9, 16}

集合中的元素必须是hashable类型,使用哈希存储的容器都会对元素提出这一要求。
所谓hashable类型指的是能够计算出哈希码的数据类型,通常不可变类型都是hashable类型,如:
整数(int)、浮点小数(float)、布尔值(bool)、字符串(str)、元组(tuple)等。

可变类型都不是hashable类型,因为可变类型无法计算出确定的哈希码,所以它们不能放到集合中。例如:我们不能将列表作为集合中的元素;同理,由于集合本身也是可变类型,所以集合也不能作为集合中的元素。
我们可以创建出嵌套的列表,但是我们不能创建出嵌套的集合,这一点在使用集合的时候一定要引起注意。

1.5 集合变量也可以赋值给另一个变量

但这时,两个变量指向相同的内存,当一个集合元素发生变化时,另一个集合的元素同时也会**发生变化**
如果需要创建的一个原集合内容一致的**不同集合对象**时,可以使用**s.copy()**的方法。

In [18]:
city_set = {'吉林', '武汉', '北京'}
new_city_set = city_set  # 将集合变量city_set赋值给new_city_set,此时两个变量指向相同的内存
city_set.add('深圳')     # 为集合city_set添加一个元素'深圳',new_city_set也会同时变化
print(city_set)
print(new_city_set)
{'北京', '武汉', '吉林', '深圳'}
{'北京', '武汉', '吉林', '深圳'}

如果需要创建的一个原集合内容一致的不同集合对象时,可以使用s.copy()的方法。

In [20]:
city_set = {'吉林', '武汉', '北京'}
new_city_set = city_set.copy()  # 创建的一个集合变量city_set内容一致的新集合对象new_city_set
city_set.add('深圳')            # 为集合city_set添加一个元素'深圳',不影响new_city_set
print(city_set)
print(new_city_set)
{'北京', '武汉', '吉林', '深圳'}
{'北京', '武汉', '吉林'}

2. 集合去重特性的应用

由于集合(set)内的数据是**不重复**的,因此集合构造器常用来对其他的序列数据进行**“去重操作”**

In [21]:
setA = set((90, 75, 88, 65, 90))
print(setA)                      # 输出{88, 65, 90, 75},重复的90被去掉
scores = [80, 85, 88, 93, 88, 81, 96, 73, 85, 77, 77, 86, 89, 68, 93, 82, 95, 81, 80, 70]
# 输出不重复的4个最低分 [68, 70, 73, 77]
# 用set()函数将score转为集合,同时去除重复的数字,list()函数再将集合转为列表
# 用sorted()函数对集合元素进行升序排序,再切片取排序后列表的前4个数
print(sorted(set(scores))[0:4])
{88, 65, 90, 75}
[68, 70, 73, 77]

集合支持Python内置函数 len(s),用于获取集合s中数据元素的个数。
可根据序列对象转换为集合后前后**长度的变化**判定其中是否存在**重复元素**

实例7.1: 奇特的四位数

一个四位数,各位数字互不相同,所有数字之和等于6,并且这个数是11的倍数。满足这种要求的四位数有多少个?各是什么?

分析
将这个四位数转为集合,如果各位上有相同数字存在,重复数字会被去掉,则生成的集合长度len(set(str(i)))必小于4,只有长度等于4的集合,其对应的数中才无重复数字。
所有数字之和等于6,并不需要从0000遍历到9999,只需要遍历到3210即可。
若这个数是11的倍数,则该数对11取模的值应该等于0。
其中map(int,list(str(i)))是将数字i 转为字符,再转为列表,map(int,ls)函数的作用是将序列ls中的每个元素映射为第一个参数指定的数据类型,此处是映射为整型。

In [ ]:
# 输出各位数字互不相同、所有数字之和等于6,且是11的倍数的4位数
ls = []
for i in range(1000,3211):  # 各位数加和为6的最大的无重复数字的数是3210
    if i % 11 == 0 and sum(map(int,str(i))) == 6 and len(set(str(i))) == 4:
        ls.append(i)        # 符合条件的数字加到列表ls里
print(len(ls))              # 列表长度就是符合条件的数字的个数 6
print(ls)                   # [1023, 1320, 2013, 2310, 3102, 3201]

实例7.2: 特殊的生日

每个日期可以转成8位数字,比如2018年5月12日 对应的就是 20180512。小明发现,自己的生日转成8位数字后,8个数字都没有重复,而且自他出生之后到今天,再也没有这样的日期了。请问小明的生日是哪天?

分析
可以从当前日期开始向前逐日判定是否为满足条件的日期,为提高效率,可以略过一些不能表示合法日期的数字,例如月份位超过12的和日期位超过31的数字,同时确保最后得到的数字是合法日期。

In [ ]:
# 从当前日期向前查找第一个没有重复数字的日期
day = 20200202                           # 当前日期,整型
while True:
   day = day - 1                        # 从当前日期向前逐日判定
   strDay = str(day)                     # 整型转字符串
   if int(strDay[4:6])>13:
      continue                    # 忽略月份超过12的数字
   elif strDay[4:6] in ['01','03','05','07','08','10','12'] and int(strDay[-2:]) > 31:
      continue                    # 以上月份,忽略超过31的日期
   elif strDay[4:6] in ['04','06','09','11'] and int(strDay[-2:]) > 30:
      continue                    # 以上月份,忽略超过30的日期
   elif strDay[4:6] in ['02'] and int(strDay[-2:]) > 19:
      continue                    # 2月份,忽略超过19的日期,20以后有重复数字
   elif len(set(strDay)) == 8:    # 字符串转集合,判断无重复数字是否8个
      print(strDay)
      break                       # 找到第一个满足条件的日期中止循环 19870625

上面的代码使用了大量的分支结构,用于排除不合法的日期,程序较复杂。
这个问题也可以借助datetime库里的一些方法实现:
获取今天日期的方法是datetime.now(),可得到形如2018-09-04的日期。
返回日期间隔的方法是timedelta(days=1),括号中的days=1表示日期间隔为1天。
从当前天向前查看每一天的日期中是否有重复数字,第一个出现的没有重复数字的日期就是答案。
判断日期中是否有重复数字可用集合方法。

使用datetime库简化后的代码如下:

In [ ]:
# 从当前日期向前查找第一个没有重复数字的日期
import datetime

todays = datetime.datetime.now()          # 获取今天日期形如:2020-02-02
while True:
    todays = todays -datetime.timedelta(days=1)   # 从今天起日期依次减一天
    sday = todays.strftime('%Y%m%d')      # 获得格式化的时间,形如:20190701
    if len(set(sday)) == 8:    # 字符串转集合,判断无重复数字是否8个
        print('出生日期是{}{}{}日'.format(sday[:4],sday[4:6],sday[6:]))  
        break                             # 找到一个符合条件的日期就结束循环

datetime是一个非常有用的内置库,几乎和日期或时间相关的操作都可以找到相关的函数或方法,遇到相关的需求可查阅datetime库官方文档获取帮助。

练一练

猜年龄

美国数学家维纳(N.Wiener)智力早熟,11岁就上了大学。他曾在1935~1936年应邀来中国清华大学讲学。一次,他参加某个重要会议,年轻的脸孔引人注目。于是有人询问他的年龄,他回答说:“我年龄的立方是个4位数。我年龄的4次方是个6位数。这10个数字正好包含了从0到9这10个数字,每个都恰好出现1次。” 请编程输出当年维纳的年龄。‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬

本题没有输入‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬,输出应为18(维纳当年的年龄)

提示:循环遍历所有可能的年龄,将年龄值的立方和4次方的结果转换为字符串并拼接起来,若该字符串转化为集合后长度长度仍为10,则是正确的年龄。

In [ ]:
# 请在此处编写代码,完成上面的题目的要求
In [ ]: