目录
第十六章 数据结构
16.1 数据结构
数据,指能够输入计算机中,由计算机所处理的元素。结构,指数据之间的关系。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。其表现为计算机存储,组织与处理数据的方式。
常用的数据结构有:
- 数组
- 链表
- 堆栈
- 队列
- 树
- 哈希表
说明:堆栈、队列、链表为线性存储结构(线性表),即多个元素的有序序列。堆栈,队列为操作受限的线性表。
16.1.1数组
数组采用的是连续存储的方式。通过其实地址与偏移量进行访问其中的每个元素。数组具有很好的随机访问特征。
16.1.2链表
链表存储的数据元素称为结点,在链表中,各个结点是有序的。每个结点分为两个部分:
- 数据域
- 指针域
根据链表指针域的数量,可以将链表分为:
- 单向链表
- 双向链表
思考:链表与数组有什么不同?
16.1.3堆栈
堆栈(简称栈)是一种操作受限的线性表,只能在表头增加或删除元素。我们通常将栈使用竖直的方式来表示,因此,表尾称为“栈顶”,表头称为“栈底”。
栈表现的特征为后进先出。(LIFO——Last In First Out)
16.1.4队列
队列是一种操作受限的线性表。只能在队尾增加元素,在队头删除元素。
队列表现的特征为先进先出(FIFO)。
当队列的两端都可以增加与删除元素时,这种队列称为双端队列。双端队列可以同时实现队列与堆栈的特征。
16.1.5树
树是由结点集及连接每对结点的有向边集组成。
如果存在有向边连接A到B,则A为B的父节点(父亲),B为A的子节点(孩子)。没有父节点的结点称为根,没有子节点的结点称为叶子。具有相同父节点的各结点称为兄弟。
从根到某一节点经过的边数称为路径长度。
二叉树是一种树形结构,其特征是任意结点的孩子数不超过两个。一棵二叉树要么为空,要么由根、左子树与右子树组成(左右子树可能为空)。
一棵深度(树的层数)为m,且具有2m-1个结点的二叉树称为满二叉树。
当一棵二叉树的所有结点编号(从上到下,从左到右进行)都与满二叉树一致时,该二叉树称为完全二叉树。
树的遍历可以分为以下三种:
- 先序遍历
- 中序遍历
- 后序遍历
说明:遍历二叉树实际上是将树的非线性结构进行线性化操作。
16.1.6哈希表
将一组关键字映射到一个有限的地址集上,这种表称为哈希(散列)表。哈希表中,关键字映射到相应的地址需要使用相关的算法,称为哈希函数。关键字映射后所得的存储地址称为哈希(散列)地址。
16.2算法
16.2.1算法概述
算法为解决特定问题而给出的一种方案描述。
算法效率的衡量:
- 时间复杂度
- 空间复杂度
16.2.2时间复杂度
时间负责度衡量的标准:
- 执行时间
- 执行规模(频度)
- 最好执行次数
- 平均执行次数
- 最坏执行次数
常用的时间复杂度:
O(1) < O(logn) < O(n) < O(nlogn) < O(n ^ 2) < O(n ^ 2 logn) < O(n3)
16.2.3空间复杂度
空间复杂度为算法所需的存储空间。
16.3查找
16.3.1顺序查找
从前到后,顺序进行查找与关键字相符的元素。
时间复杂度:O(n)【O(1)】
16.3.2折半查找
选择中间的元素,进行比较,每次排除一半的元素。
时间复杂度:O(logn)【O(1)】。
思考:折半查找一定好于顺序查找吗?
16.4排序
从稳定性上将,排序可以分为两类:
- 稳定排序
- 不稳定排序
16.4.1冒泡排序
冒泡排序是进行两两比较,每次选出一个最大(小)元素,共执行n – 1次。
时间复杂度:最好:O(n) 平均:O(n2) 最差:O(n2)
稳定性:稳定。
16.4.2选择排序
每次选择一个最小(大)的元素,放入数组的最前端。共执行n – 1次。
时间复杂度:最好:O(n2) 平均:O(n2) 最差:O(n2)
稳定性:不稳定。
16.4.3插入排序
从第二个元素开始,插入到现有数组中,每个元素都保证插入之后,数组是有序的。
时间复杂度:最好:O(n) 平均:O(n2) 最差:O(n2)
稳定性:稳定。
从插入排序时间复杂度可知,插入排序适合于基本有序的情况。
16.4.4希尔排序
希尔排序(也称缩小增量排序),根据一定的增量,将记录分组,然后对每组进行插入排序。
因为在基本有序的情况下,插入排序的时间复杂度可以从O(n2)提升至O(n),故希尔排序是插入排序的一种改进版。
希尔排序的最优增量序列是一个非常复杂的问题,没有固定的选择原则,但通常情况下,应该令增量序列的每次减半,并且增量序列的最后一个值一定是1。
时间复杂度:最好:与间隔相关 平均:与间隔相关 最差:O(n2)
稳定性:不稳定。
16.4.5快速排序
从数组中选出一个中心点(pivot),使得中心点左侧的元素都小于(大于)该元素,中心点右侧的元素都大于(小于)该元素。然后针对中心点分割的这两个区间,继续执行递归的快速排序,直到整个数组区间全部有序。
时间复杂度:最好:O(nlogn) 平均:O(nlogn) 最差:O(n2)
稳定性:不稳定。
16.4.6归并排序
归并排序为将两个有序的数据集合并成一个有序的数据集。
最好:O(nlogn) 平均:O(nlogn) 最差:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
第17章 线程
17.1线程与进程的关系
程序是一些列的指令集,程序是静态的,当程序运行时,就会创建一个进程。
线程是进程的基本执行单元,一个进程至少要具有一个线程。
17.2多线程
多线程,就类似与操作系统中的多进程。简单的讲,就是可以同时并发执行多个任务,处理多件事情。这与我们经常所谓的边唱边跳,边说边做事一个道理。
17.2.1计算密集与IO密集
计算密集,是指在程序运行中,更多的时间为CPU在进行计算。而IO密集,则是更多的时间,在进行输入输出。
Python中,threading模块提供了关于线程的相关操作。
17.2.2线程的创建
我们可以采用两种方式来创建线程:
- 使用threading模块的Thread类,通过指定target与args(可选)参数。
- 通过继承Thread类,重写run方法。
思考:直接调用run与调用start方法有什么不同?
线程的生命周期
线程的生命周期可以分为以下环节:
- 新建:创建对象
- 就绪:调用start后
- 运行:获得CPU资源
- 阻塞(挂起):失去CPU资源
- 死亡:线程执行结束或抛出未捕获的异常
17.2.3线程相关操作
threading模块相关功能:
- threading.active_count()
- threading.enumerate()
- threading.current_thread()
- threading.get_ident()
- threading.main_thread()
线程对象功能:
- start()
- run()
- join(timeout=None)
- name setName / getName
- ident
- is_alive()
- daemon isDaemon() / setDaemon()
17.3线程同步
17.3.1并发修改的问题
当多线程并发运行时,多线程间很可能操作共享成员变量,此时,就需要对共享成员变量的操作进行同步,避免出现多线程的并发修改而引起的意外错误。
程序:多人同时购票。
17.3.2线程锁
我们可以通过threading.Lock()获得线程锁,来实现多个线程对共享区域的互斥访问。线程锁提供两个方法:
acquire():尝试去获得锁。
release():释放所获得的锁。
注意:要确保锁得到有效的释放。
17.3.3死锁
当两个或多个线程同时拥有自己的资源,而相互等待获得对方资源,导致程序永远陷入僵持状态,这就是死锁。
当多线程并发访问共享数据时,使用线程锁可以避免多线程对共享变量并发修改带来的危害,但同时有可能会产生死锁。
17.4线程队列
queue模块提供了队列的功能,该模块具有三个类:
- queue.Queue(maxsize=0)
- queue.LifoQueue(maxsize=0)
- queue.PriorityQueue(maxsize=0)
队列类实现了内部的锁机制,因此,队列类型可以安全的用于多线程并发操作中。
其中,参数maxsize表示队列的最大元素个数,如果传递0或者负值,则表示无限容量。如果队列达到了最大容量,将会进行阻塞,直到有元素删除为止。
队列对象具有的方法如下:
- qsize()
- empty()
- full()
- put(item, block=True, timeout=None)
- put_nowait(item) put(item, False)
- get(block=True, timeout=None)
- get_nowait() get(False)
第18章 进程
在Python中,由于GIL的存在,我们在多线程环境下,无法充分利用多核多CPU带来的任何优势,因此,我们可以使用多进程来代替多线程,这样就可以不用受到GIL的制约。
18.1进程的创建(Process类)
Python中提供了multiprocessing模块来实现进程的相关操作。
我们可以通过与创建线程类似的方式创建子进程:
- 通过Process类来创建一个子进程。
- 通过继承Process类,重写run方法来创建子进程。
注意:在Windows平台,在创建子进程时,一定要使用如下的语法:
if __name__ == "__main__":
# 创建子进程语句,例如:
p = multiprocessing.Process(target=…)
原因在于:在Windows平台上,当创建子进程时,会自动导入创建子进程的模块,因此,这样就会出现无限递归调用。为了避免这种情况,我们使用如上的判断进行限制。在Linux中无此限制。
练习:创建一个子进程,在if __name__ == "__main__"平行的级别上面,输出“当前进程执行完毕”,会发现什么情况?
18.2进程相关的操作
- os.getpid()
- os.getppid()
- os.fork()
进程对象的相关操作:
- run()
- start()
- is_alive()
- name
- daemon
- pid
18.3进程的创建(fork方法)
在Linux操作系统中,我们也可以通过os模块的fork方法来创建子进程。fork方法会返回一个数值,对于父进程,会返回子进程的id,对于父进程,会返回0。
当创建子进程后,子进程会在fork的位置,继续向下执行。
18.4进程队列
进程具有独立的内存空间,因此,进程通讯不像线程通讯那样容易。
我们可以使用进程队列来完成进程之间的通讯。进程队列的用法与线程队列非常相似。
第19章 网络编程
19.1网络基础
19.1.1计算机网络
计算机网络,是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,从而实现信息与资源共享的计算机系统。
网络分为如下的七层结构:
19.1.2计算机通讯协议
计算机网络中的计算机可以相互间进行信息交流。而要进行交流,就需要具有一定的规则,我们称之为通讯协议。可以认为,协议就是一种规范。
相关协议:
-
- HTTP(HyperText Transfer Protocol )超文本传输协议
- IP(Internet Protocol)互联网协议
- TCP(Transmission Control Protocol)传输控制协议
- UDP(User Datagram Protocol)用户数据报协议
- FTP(File Transfer Protocol)文件传输协议
TCP/IP协议经常放在一起使用。在TCP/IP协议下,网络通常也分成如下四层的结构:
TCP与UDP协议
- TCP协议是基于连接的协议,该协议提供点对点的通道,在两台计算机设备间进行可靠的数据传输。TCP协议保证一方发送的数据可以准确的被另一方接收,并且接收数据的顺序与发送的顺序一致。(HTTP,FTP)
- UDP协议是基于数据报的协议(不是基于连接的协议),该协议发送独立的数据报(数据包),各个数据报之间不受影响。UDP协议不保证接收端一定会接收到发送端发送的数据报,同时,也不保证接收数据报的顺序与发送的顺序一致。(ping)
19.1.3IP地址与端口号
在计算机网络中,可能会存在多台计算机,我们使用IP地址来区分每台计算机。可以认为,IP地址就是每台计算机在计算机网络中的唯一标识。IP地址使用32位数据来表示。我们习惯上将IP地址分为4段,每段8位。(0~255)
端口号用来标识不同的程序。端口号使用16位数据来表示。(0~65535)其中,0~1023端口被常用的系统服务所占用。我们使用的服务不应使用这些端口。
数据传输时,会携带IP地址与端口号信息。其中,IP地址用来识别数据传输到哪台计算机,而端口号用来识别,传输到计算机的哪个应用程序。
19.2URL
19.2.1基本组成
URL(Uniform Resource Locator)统一资源定位,为一个字符串序列,引用网络上的一个资源地址,该地址可以是文件,目录,数据库查询等。
URL分为两个部分:(二者使用://进行分隔)
- 协议标识符 使用哪种协议获取相关资源
- 资源名称 资源的详细地址
例如:http://www.somewebsite.com
其中,资源名称的格式取决于所使用的协议,大部分协议的格式如下:
- 主机名称
- 文件名称(路径)
- 端口号
- 相关引用
例如:http://127.0.0.1:8080/index.html#bak
说明:通常主机名称与文件名称是必须的,而端口号与相关引用是可选的。
19.2.2解析URL
我们可以通过urlparse函数来对指定的URL进行解析,结果分为六部分:
- 协议
- 网络地址
- 路径
- 路径参数
- 查询参数
- 引用片段
函数返回ParseResult类型的对象(元组的子类型),我们可以通过索引(或者属性名)来访问对应的部分。
19.2.3发起请求
我们可以向指定的URL发送请求,获得服务端的回应信息。
访问本地资源。
下载指定的URL资源。
19.3TCP
网络上的两个程序建立双向的通讯连接,这个连接的一端称为一个socket,每个socket会绑定一个特定的端口号。客户端与服务端通过socket进行数据读取与写入。
我们可以使用socket模块提供的功能,来实现TCP与UDP协议下的网络通讯。
19.3.1UDP
数据报是相互独立的信息,通过网络发送。对于接收端,是否能接收,接收时间,接收的顺序都是不确定的。
19.3.2协程
协程是比线程更小的执行单元。比起线程,协程的切换更加简单容易,减少性能消耗。
之前学习的生成器,就可以实现协程。