1. 算法:算法是计算机处理信息的本质,因为计算机程序本质上是一个算法来告诉计算机确切的步骤来执行一个指定的任务。一般的,当算法在处理信息时,会从输入设备或数据的存储地址读取数据,把结果写入输出设备或某个存储地址以后再调用。
算法是独立存在的一种解决问题的方法和思想
2. 算法的五大特性:
输入:算法具有0个或多个输入
输出:算法至少有1个或多个输出
有穷性:算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
确定性:算法中的每一步都有确定的含义,不会出现二义性
可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成
3. 算法效率衡量:
单纯依靠运行的时间来比较算法的优劣并不一定是客观准确的!
4. 时间复杂度与“大O记法”
每台机器执行的总时间不同,但是执行基本运算数量大体相同
时间复杂度:执行基本运算的数量
大O记法:对于单调的整数函数f,如果存在一个整数函数g和实常数c>0,使得对于充分大的n总有f(n)<=c*g(n),就说函数g是f的一个渐进函数(忽略常数),记为f(n) =O(g(n))。也就是说,在趋向于无穷的极限意义下,函数f的增长速度会受到函数g的约束,亦即函数f与函数g的特征相似。
5. 最坏时间复杂度:
最优时间复杂度:算法完成工作最少需要多少基本操作。价值不大,因为它没有提供什么有用的信息。
最坏时间复杂度:算法完成工作最多需要多少基本操作。提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作
平均时间复杂度:算法完成工作平均需要多少基本操作。对算法的一个全面评价,因为它完整全面的反映了这个算法的性质。但另一个方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成的。
6. 时间复杂度的几条基本计算规则:
基本操作,即只有常数项,认为其时间复杂度为O(1)
顺序结构,时间复杂度按加法进行计算
循环结构,时间复杂度按乘法进行计算
分支结构,时间复杂度取最大值
判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
在没有特殊说明时,所分析的算法的时间复杂度都是指最坏时间复杂度
7. 常见时间复杂度之间的关系
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
8. Python内置类型性能分析
timeit模块:可以用来测试一小段Python代码的执行速度
class timeit.Timer(stmt=’pass’, setup=’pass’, timer=<timer function>)
Timer是测量小段代码执行速度的类
stmt参数时要测试的代码语句
setup参数是运行代码时需要的设置
timer是一个定时器函数,与平台有关
timeit.Timer.timeit(number=1000000)
Timer类中测试语句执行速度的对象方法。Number参数是测试代码时的测试次数,默认为1000000次。方法返回执行代码的平均耗时,一个float类型的秒数。
from timeit import Timer
def test1():
li = []
for i in range(10000):
li.append(i)
timer1 = Timer('test1()', ''from __main__ import test1')
print(timer1.timeit(1000))
9. list内置操作的时间复杂度
10. dict内置操作的时间复杂度
11. 数据结构指数据对象中数据元素之间的关系
12. 算法与数据结构的区别:
数据结构只是静态的描述了数据元素之间的关系
搞笑的程序需要在数据结构的基础上设计和选择算法
程序 = 数据结构 + 算法
总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
13. 抽象数据类型(Abstract Data Type):指一个数学模型以及定义在此数学模型上的一组操作,即把数据类型和数据类型上的运算捆在一起,进行封装。引入抽象数据类型的目的是把数据类型的表示和数据类型上运算的实现与这些数据类型和运算在程序中的引用隔开,使他们相互独立。
最常用的数据运算:插入、删除、修改、查找、排序
14. 顺序表:将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示
链表:将元素存放在通过链接构造起来的一系列存储块中
15. 顺序表的基本形式:
图a表示的是顺序表的基本形式,数据元素本身连续存储,每个元素所占的存储单元大小固定相同,元素的下标是其逻辑地址,而元素存储的物理地址(实际内存地址)可以通过存储区的起始地址Loc (e0)加上逻辑地址(第i个元素)与存储单元大小(c)的乘积计算而得,即:
Loc(ei) = Loc(e0) + c * i
故,访问指定元素时无需从头遍历,通过计算便可获得对应地址,其时间复杂度为O(1)
如果元素的大小不统一,则须采用图b的元素外置的形式,将实际数据元素另行存储,而顺序表中各单元位置保存对应元素的地址信息(即链接)。由于每个链接所需的存储量相同,通过上述公式,可以计算出元素链接的存储位置,而后顺着链接找到实际存储的数据元素。注意,图b中的c不再是数据元素的大小,而是存储一个链接地址所需的存储量,这个量通常很小。
图b这样的顺序表也被称为对实际数据的索引,这是最简单的索引结构
16. 顺序表的结构:一个顺序表的完整信息包括两部分,一部分是表中的元素集合,另一部分是为实现正确操作而需记录的信息,即有关表的整体情况的信息,这部分信息主要包括元素存储区的容量和当前表中已有的元素个数两项。
17. 顺序表的两种基本实现方式
图a为一体式结构,存储表信息的单元与元素存储区以连续的方式安排在一块存储区里,两部分数据的整体形成一个完整的顺序表对象。
一体式结构整体性强,易于管理。但是由于数据元素存储区域是表对象的一部分,顺序表创建后,元素存储区就固定了。
图b为分离式结构,表对象里只保存与整个表有关的信息(即容量和元素个数),实际数据元素存放在另一个独立的元素存储区里,通过链接与基本表对象关联。
18. 元素存储区替换
一体式结构由于顺序表信息区与数据区连续存储在一起,所以若想更换数据区,则只能整体搬迁,即整个顺序表对象(指存储顺序表的结构信息的区域)改变了。
分离式结构若想更换数据区,只需将表信息区中的数据区链接地址更新即可,而该顺序表对象不变。
19. 元素存储区扩充
采用分离式结构的顺序表,若将数据区更换为存储空间更大的区域,则可以在不改变表对象的前提下对其数据存储区进行了扩充,所有使用这个表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储,这种表结构就不会因为满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
扩充的两种策略:
(1)每次扩充增加固定数目的存储位置,如每次扩充增加10个元素位置,这种策略可称为线性增长。
特点:节省空间,但是扩充操作频繁,操作次数多。
(2)每次扩充容量加倍,如每次扩充增加一倍存储空间。
特点:减少了扩充操作的执行次数,但可能会浪费空间资源。以空间换时间,推荐的方式。
20. 顺序表的操作
增加元素:
a. 尾端加入元素,时间复杂度为O(1)
b. 非保序的加入元素(不常见),时间复杂度为O(1)
c. 保序的元素加入,时间复杂度为O(n)
删除元素:
a. 删除表尾元素,时间复杂度为O(1)
b. 非保序的元素删除(不常见),时间复杂度为O(1)
c. 保序的元素删除,时间复杂度为O(n)
21. Python中的顺序表
Python中的list和tuple两种类型采用了顺序表的实现技术,具有前面讨论的顺序表的所有性质。
tuple是不可变类型,即不变的顺序表,因此不支持改变其内部状态的任何操作,而其他方面,则与list的性质类似。
list的基本实现技术
Python标准类型list就是一种元素个数可变的线性表,可以加入和删除元素,并在各种操作中维持已有元素的顺序(即保序),而且还具有以下行为特征:
(1)基于下标(位置)的高效元素访问和更新,时间复杂度应该是O(1);
为满足该特征,应该采用顺序表技术,表中元素保存在一块连续的存储区中。
(2)允许任意加入元素,而且在不断加入元素的过程中,表对象的标识(函数id得到的值)不变。
为满足该特征,就必须能更换元素存储区,并且为保证更换存储区时list对象的标识id不变,只能采用分离式实现技术。
在Python的官方实现中,list就是一种采用分离式技术实现的动态顺序表。这就是为什么用list.append(x) (或 list.insert(len(list), x),即尾部插入)比在指定位置插入元素效率高的原因。
在Python的官方实现中,list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
22. 链表:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是不像顺序表一样连续存储数据,而是在每一个节点(包括数据区和链接区)(数据存储单元)里存放下一个节点的位置信息(即地址)。
23. 单向链表:
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(元素域)和一个链接域。这个链接指向链表中的下一个节点,而最后一个节点的链接域则指向一个空值。
表元素域elem用来存放具体的数据。
链接域next用来存放下一个节点的位置(python中的标识)
变量p指向链表的头节点(首节点)的位置,从p出发能找到表中的任意节点。
尾节点的链接域为空
节点实现:
class SingleNode(object):
'''单链表的节点'''
def __init__(self, item):
# _item 存放数据元素
self.item = item
# _next 是下一个节点的标识
self.next = None
单链表的操作:
is_empty() 链表是否为空
length() 链表长度
travel() 遍历整个链表
add(item) 链表头部添加元素
append(item) 链表尾部添加元素
insert(pos, item) 指定位置添加元素
remove(item) 删除节点
search(item) 查找节点是否存在
class SingleLinkList(object):
'''单链表'''
def __init__(self, node=None):
self.__head = node
def is_empty(self):
'''链表是否为空'''
return self.__head is None
def length(self):
'''链表长度'''
# cur游标,用来移动遍历节点
cur = self.__head
# count 记录数量
count = 0 # 若初始值设为1,当输入空列表时,返回值错误
while cur is not None:
count += 1
cur = cur.next
return count
def travel(self):
'''遍历整个链表'''
cur = self.__head
while cur != None:
print(cur.elem, end='')
cur = cur.next
def add(self, item):
'''链表头部添加元素,头插法'''
node = Node(item)
node.next = self.__head
self.__head = node
def append(self, item):
'''链表尾部添加元素,尾插法'''
node = Node(item) # item为用户想要传入的数据
if self.is_empty():
self.__head = node
else:
cur = self.__head
while cur.next is not None:
cur = cur.next
cur.next = node
def insert(self, pos, item):
'''指定位置添加元素'''
if pos <= 0:
self.add(item)
elif pos > self.length()-1:
self.append(item)
else:
pre = self.__head
count = 0
while count < (pos - 1):
count += 1
pre = pre.next
# 当循环退出后,pre指向pos-1位置
node = Node(item)
node.next = pre.next
pre.next = node
def remove(self, item):
'''删除节点'''
cur = self.__head
pre = None
while cur is not None:
if cur.elem == item:
# 先判断此节点是否是头节点
if cur == self.__head:
self.__head = cur.next
else:
pre.next = cur.next
break
else:
pre = cur
cur = cur.next
def search(self, item):
'''查找节点是否存在'''
cur = self.__head
while cur != None:
if cur.elem == item:
return True
else:
cur = cur.next
return False
24. 链表与顺序表的对比
链表失去了顺序表随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大,但对存储空间的使用要相对灵活。
链表与顺序表的各种操作复杂度如下所示:
操作 | 链表 | 顺序表 |
访问元素 | O(n) | O(1) |
在头部插入/删除 | O(1) | O(n) |
在尾部插入/删除 | O(n) | O(1) |
在中间插入/删除 | O(n) | O(n) |
注意虽然表面看起来复杂度都是 O(n),但是链表和顺序表在插入和删除时进行的是完全不同的操作。链表的主要耗时操作是遍历查找,删除和插入操作本身的复杂度是O(1)。顺序表查找很快,主要耗时的操作是拷贝覆盖。因为除了目标元素在尾部的特殊情况,顺序表进行插入和删除时需要对操作点之后的所有元素进行前后移位操作,只能通过拷贝和覆盖的方法进行。
25. 双向链表
一种更复杂的链表是“双向链表”或“双面链表”。每个节点有两个链接:一个指向前一个节点(前驱节点),当此节点为第一个节点时,指向空值;而另一个指向下一个节点(后继节点),当此节点为最后一个节点时,指向空值
操作:
is_empty() 链表是否为空
length() 链表长度
travel() 遍历链表
add(item) 链表头部添加
append(item) 链表尾部添加
insert(pos, item) 指定位置添加
remove(item) 删除节点
search(item) 查找节点是否存在
实现:
class Node(object):
'''节点'''
def __init__(self, item):
self.elem = item
self.next = None
self.prev = None
# 前几个方法和单链表一样,因此可以采用继承
class DoubleLinkList(object):
'''双链表'''
def __init__(self, node=None):
self.__head = node
def is_empty(self):
'''链表是否为空'''
return self.__head is None
def length(self):
'''链表长度'''
# cur游标,用来移动遍历节点
cur = self.__head
# count 记录数量
count = 0 # 若初始值设为1,当输入空列表时,返回值错误
while cur is not None:
count += 1
cur = cur.next
return count
def travel(self):
'''遍历整个链表'''
cur = self.__head
while cur != None:
print(cur.elem, end='')
cur = cur.next
def add(self, item):
'''链表头部添加元素,头插法'''
node = Node(item)
node.next = self.__head
self.__head = node
node.next.prev = node
def append(self, item):
'''链表尾部添加元素,尾插法'''
node = Node(item) # item为用户想要传入的数据
if self.is_empty():
self.__head = node
else:
cur = self.__head
while cur.next is not None:
cur = cur.next
cur.next = node
node.prev = cur
def insert(self, pos, item): '''指定位置添加元素''' if pos <= 0: self.add(item) elif pos > self.length()-1 self.append(item) else: cur = self.__head count = 0 while count < pos: count += 1 cur = cur.next # 当循环退出后,cur指向pos位置 node = Node(item) node.next = cur node.prev = cur.prev cur.prev.next = node cur.prev = node
def remove(self, item): '''删除节点''' cur = self.__head while cur is not None if cur.elem == item: # 先判断此节点是否是头节点 if cur == self.__head: self.__head = cur.next if cur.next: # 判断链表是否只有一个节点 cur.next.prev = None else: cur.prev.next = cur.next if cur.next: # 判断该节点是否为最后一个节点 cur.next.prev = cur.prev break else: pre = cur cur = cur.next def search(self, item): '''查找节点是否存在''' cur = self.__head while cur is not None: if cur.elem == item: return True else: cur = cur.next return False
26. 单向循环链表
单链表的一个变形是单向循环链表,链表中最后一个节点的next域不再为None,而是指向链表的头节点。
操作:
is_empty() 判断链表是否为空
length() 返回链表的长度
travel() 遍历
add(item) 在头部添加一个节点
append(item) 在尾部添加一个节点
insert(pos, item) 在指定位置pos添加节点
remove(item) 删除一个节点
search(item) 查找节点是否存在
实现:
class Node(object): '''节点''' def __init__(self, elem): self.elem = elem self.next = None
class SinCycLinkedList(object): '''单向循环链表''' def __init__(self, node=None): self.__head = node if node: node.next = node def is_empty(self): '''链表是否为空''' return self.__head is None def length(self): '''链表长度''' if self.is_empty(): return 0 # cur游标,用来移动遍历节点 cur = self.__head # count 记录数量 count = 1 while cur.next != self.__head: count += 1 cur = cur.next return count def travel(self): '''遍历整个链表''' if self.is_empty(): return 0 cur = self.__head while cur.next != self.__head: print(cur.elem, end='') cur = cur.next # 退出循环,cur指向尾节点,但尾节点的元素未打印 print(cur.elem) def add(self, item): '''链表头部添加元素,头插法''' node = Node(item) if self.is_empty(): self.__head = node node.next = node cur = self.__head while cur.next != self.__head: cur = cur.next # 退出循环,cur指向尾节点 node.next = self.__head self.__head = node cur.next = node # 尾节点指向头结点,构成循环 def append(self, item): '''链表尾部添加元素,尾插法''' node = Node(item) # item为用户想要传入的数据 if self.is_empty(): self.__head = node node.next = node else: cur = self.__head while cur.next is not self.__head: cur = cur.next cur.next = node node.next = self.__head def insert(self, pos, item): '''指定位置添加元素''' if pos <= 0: self.add(item) elif pos > self.length() -1: self.append(item) else: pre = self.__head count = 0 while count < (pos - 1): count += 1 pre = pre.next # 当循环退出后,pre指向pos-1位置 node = Node(item) node.next = pre.next pre.next = node def remove(self, item): '''删除节点''' if self.is_empty(): return None cur = self.__head pre = None while cur.next != self.__head: if cur.elem == item: # 先判断此节点是否是头节点 # 如果是头节点,需要找到尾节点,构成循环 if cur == self.__head: rear = self.__head while rear.next != self.__head: rear = rear.next # 此时cur游标指向头结点,rear游标指向尾节点 self.__head = cur.next rear.next = cur.next else: # 中间节点 pre.next = cur.next # 此时不应该使用break,使用break代表整个函数结束, # 程序不会继续往下执行 return else: pre = cur cur = cur.next # 退出循环,cur指向尾节点 if cur.elem == item: if cur == self.__head: # 列表只有一个节点 self.__head = None else: pre.next = cur.next def search(self, item): '''查找节点是否存在''' if self.is_empty(): return False cur = self.__head while cur.next != self.__head: if cur.elem == item: return True else: cur = cur.next # 退出循环,cur指向尾节点 if cur.elem == item: return True return False
27. 栈:
栈(stack),有些地方称为堆栈,是一种容器,可存入数据元素、访问元素、删除元素,它的特点在于只能允许在容器的一端(称为栈顶端指标,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。没有了位置概念,保证任何时候可以访问、删除的元素都是此前最后存入的那个元素,确定了一种默认的访问顺序。
由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
28. 栈的实现:
栈可以用顺序表实现,也可以用链表实现。
Stack() 创建一个新的空栈
push(item) 添加一个新的元素item到栈顶
pop() 弹出栈顶元素
peek() 返回栈顶元素
is_empty() 判断栈是否为空
size() 返回栈的元素个数
class Stack(object): """栈""" def __init__(self): self.items = [] def is_empty(self): """判断是否为空""" return self.__list == [] def push(self, item): """加入元素""" self.items.append(item) def pop(self): """弹出元素""" return self.items.pop() def peek(self): """返回栈顶元素""" if self.__list: return self.__list[-1] else: return None def size(self): """返回栈的大小""" return len(self.items)
29. 队列:队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!假设队列是q=(a1,a2,……,an),那么a1就是队头元素,而an是队尾元素。这样我们就可以删除时,总是从a1开始,而插入时,总是在队列最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后。
操作:
Queue() 创建一个空的队列
enqueue(item) 往队列中添加一个item元素
dequeue() 从队列头部删除一个元素
is_empty() 判断一个队列是否为空
size() 返回队列的大小
实现:
class Queue(object):
"""队列"""
def __init__(self):
self.__items = []
def is_empty(self):
return self.__items == []
def enqueue(self, item):
"""往队列中添加一个item元素"""
self.__items.append(item)
# 在头部添加元素,就应该在尾部弹出
# 顺序表中,头部添加/删除 O(n),尾部添加/删除O(1)
# 根据队列使用的经常性操作,选取相应语句,减少最坏时间复杂度
# self.__items.insert(0, item)
def dequeue(self):
"""从队列头部删除一个元素"""
return self.__items.pop(0)
# 根据队列使用的经常性操作,选取相应语句,减少最坏时间复杂度
# return self.__items.pop()
def size(self):
"""返回大小"""
return len(self.__items)
30. 双端队列:双端队列(deque,全名double-ended queue),是一种具有队列和栈的性质的数据结构。
双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。双端队列可以在队列任意一端入队和出队。
操作:
Deque() 创建一个空的双端队列
add_front(item) 从队头加入一个item元素
add_rear(item) 从队尾加入一个item元素
remove_front() 从队头删除一个item元素
remove_rear() 从队尾删除一个item元素
is_empty() 判断双端队列是否为空
size() 返回队列的大小
实现:
class Queue(object): """双端队列""" def __init__(self): self.__items = [] def is_empty(self): return self.__items == [] def add_front(self, item): """往队列中添加一个item元素""" self.__items.insert(0, item) def add_rear(self, item): """往队列中添加一个item元素""" self.__items.append(item) def remove_front(self): """从队列头部删除一个元素""" return self.__items.pop(0) def remove_rear(self): """从队列尾部删除一个元素""" return self.__items.pop() def size(self): """返回大小""" return len(self.__items)
31. 排序:排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法。
32. 排序算法的稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序。
(4, 1) (3, 1) (3, 7)(5, 6)
在这个状况下,有可能产生两种不同的结果,一个是让相等键值的纪录维持相对的次序,而另外一个则没有:
(3, 1) (3, 7) (4, 1) (5, 6) (维持次序)
(3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)
不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。不稳定排序算法可以被特别地实现为稳定。作这件事情的一个方式是人工扩充键值的比较,如此在其他方面相同键值的两个对象间之比较,(比如上面的比较中加入第二个标准:第二个键值的大小)就会被决定使用在原先数据次序中的条目,当作一个同分决赛。然而,要记住这种次序通常牵涉到额外的空间负担。
33. 冒泡排序:(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
(1)比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序的分析:
交换过程图示(第一次):
那么需要进行n-1次冒泡过程,每次对应的比较次数如下图所示
冒泡的实现:
def bubble_sort(alist):
'''冒泡排序'''
for j in range(0, len(alist) - 1):
count = 0
for i in range(0, len(alist) - 1 - j):
if alist[i] > alist[i + 1]:
alist[i], alist[i + 1] = alist[i + 1], alist[i]
count += 1
if count == 0:
return
时间复杂度:
最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
最坏时间复杂度:O(n2)
稳定性:稳定
34. 选择排序:选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
选择排序分析:
排序过程:
def select_sort(alist):
'''选择排序'''
n = len(alist)
for j in range(n - 1): # j: 0 ~ n-2
min_index = j
for i in range(j + 1, n):
if alist[min_index] > alist[i]:
min_index = i
alist[j], alist[min_index] = alist[min_index], alist[j]
时间复杂度:
最优时间复杂度:O(n2)
最坏时间复杂度:O(n2)
稳定性:不稳定(考虑升序每次选择最大的情况)
35. 插入排序:(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序分析:
def insert_sort(alist):
'''插入排序'''
n = len(alist)
for i in range(1, n):
while i > 0:
if alist[i] < alist[i - 1]:
alist[i], alist[i - 1] = alist[i - 1], alist[i]
i -= 1
else:
break
时间复杂度:
最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
最坏时间复杂度:O(n2)
稳定性:稳定
36. 快速排序:快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤:
(1)从数列中挑出一个元素,称为"基准"(pivot),
(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
def quick_sort(alist, start, end): '''快速排序''' if start >= end: # 递归的退出条件 return # 设定起始元素为要寻找位置的基准元素 mid = alist[start] # low为序列左边的由左向移动的游标 low = start # high为序列右边的由右向左移动的游标 high = end while low < high: # 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动 while low < high and alist[high] >= mid: high -= 1 # 将high指向的元素放到low的位置上 alist[low] = alist[high] # 如果low与high未重合,high指向的元素比基准元素小,则low向右移动 while low < high and alist[low] < mid: low += 1 # 将low指向的元素放到high的位置上 alist[high] = alist[low] # 退出循环后, low与high重合,此时所指位置为基准元素的正确位置 # 将基准元素放到该位置 alist[low] = mid # 对基准元素左边的子序列进行快速排序 quick_sort(alist, start, low - 1) # 对基准元素右边的子序列进行快速排序 quick_sort(alist, low + 1, end)
时间复杂度:
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(n2)
稳定性:不稳定
从一开始快速排序平均需要花费O(n log n)时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。
在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。
37. 希尔排序:希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。
例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序(此时就是简单的插入排序了)
希尔排序的分析:
def shell_sort(alist):
'''希尔排序'''
n = len(alist)
gap = n // 2
while gap >= 1:
for i in range(gap, n):
while i > 0:
if alist[i] < alist[i - gap]:
alist[i], alist[i - gap] = alist[i - gap], alist[i]
i -= gap
else:
break
gap //= 2
时间复杂度:
最优时间复杂度:根据步长序列的不同而不同
最坏时间复杂度:O(n2)
稳定想:不稳定
38. 归并排序:归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
def merge_sort(alist): '''归并排序''' n = len(alist) if n <= 1: return alist mid = n // 2 # left采用归并排序后形成的有序的新的列表 left_li = merge_sort(alist[: mid]) # right采用归并排序后形成的有序的新的列表 right_li = merge_sort(alist[mid:]) left_pointer, right_pointer = 0, 0 result = [] while left_pointer < len(left_li) and right_pointer < len(right_li): if left_li[left_pointer] < right_li[right_pointer]: result.append(left_li[left_pointer]) left_pointer += 1 else: result.append(right_li[right_pointer]) right_pointer += 1 result += left_li[left_pointer:] result += right_li[right_pointer:] return result
时间复杂度:
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
稳定性:稳定
39. 常见排序算法效率比较
40. 搜索:搜索是在一个项目集合中找到一个特定项目的算法过程。搜索通常的答案是真的或假的,因为该项目是否存在。 搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找
41. 二分法查找:二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
def binary_search(alist, item):
'''二分法查找(递归)'''
n = len(alist)
if n > 0:
mid = n // 2
if alist[mid] == item:
return True
elif item < alist[mid]:
return binary_search(alist[:mid], item)
else:
return binary_search(alist[mid + 1:], item)
return False
def binary_search1(alist, item):
'''二分法查找(非递归)'''
n = len(alist)
first = 0
last = n - 1
while first <= last:
mid = (first + last) // 2
if alist[mid] == item:
return True
elif alist[mid] > item:
last = mid - 1
else:
first = mid + 1
return False
时间复杂度:
最优时间复杂度:O(1)
最坏时间复杂度:O(logn)
42. 树:是一种抽象数据类型(ADT)或是实作这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
(1)每个节点有零个或多个子节点;
(2)没有父节点的节点称为根节点;
(3)每一个非根节点有且只有一个父节点;
(4)除了根节点外,每个子节点可以分为多个不相交的子树;
树的术语:
节点的度:一个节点含有的子树的个数称为该节点的度;
树的度:一棵树中,最大的节点的度称为树的度;
叶节点或终端节点:度为零的节点;
父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:父节点在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;
树的种类:
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树;
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树);
霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。
树的存储与表示:
顺序存储:将数据结构存储在固定的数组中,然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链式存储。
链式存储:
由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数最多为2
常见的一些树的应用场景:
(1)xml,html等,那么编写这些东西的解析器的时候,不可避免用到树
(2)路由协议就是使用了树的算法
(3)mysql数据库索引
(4)文件系统的目录结构
(5)所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构
43. 二叉树:二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)
二叉树的性质(特性):
性质1: 在二叉树的第i层上至多有2^(i-1)个结点(i>0)
性质2: 深度为k的二叉树至多有2^k - 1个结点(k>0)
性质3: 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
性质4:具有n个结点的完全二叉树的深度必为 log2(n+1)
性质5:对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)
(1)完全二叉树——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
(2)满二叉树——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
class Node(object):
def __init__(self, item):
self.elem = item
self.lchid = None
self.rchild = None
class Tree(object):
'''二叉树'''
def __init__(self):
self.root = None
def add(self, item):
node = Node(item)
if self.root is None:
self.root = node
return
queue = [self.root]
while queue:
cur_node = queue.pop(0)
if cur_node.lchid is None:
cur_node.lchid = node
return
else:
queue.append(cur_node.lchid)
if cur_node.rchild is None:
cur_node.rchild = node
return
else:
queue.append(cur_node.rchild)
44. 二叉树的遍历:树的遍历是树的一种重要的运算。所谓遍历是指对树中所有结点的信息的访问,即依次对树中每个结点访问一次且仅访问一次,我们把这种对所有节点的访问称为遍历(traversal)。那么树的两种重要的遍历模式是深度优先遍历和广度优先遍历,深度优先一般用递归,广度优先一般用队列。一般情况下能用递归实现的算法大部分也能用堆栈来实现。
深度优先遍历:对于一颗二叉树,深度优先搜索(Depth First Search)是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。
那么深度遍历有重要的三种方法。这三种方式常被用于访问树的节点,它们之间的不同在于访问每个节点的次序不同。这三种遍历分别叫做先序遍历(preorder),中序遍历(inorder)和后序遍历(postorder)。我们来给出它们的详细定义,然后举例看看它们的应用。
先序遍历:在先序遍历中,我们先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树
根节点->左子树->右子树
def preorder(self, node):
'''先序遍历'''
if node is None:
return
print(node.elem, end=' ')
self.preorder(node.lchid)
self.preorder(node.rchild)
中序遍历;在中序遍历中,我们递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树
左子树->根节点->右子树
def inorder(self, node):
'''中序遍历'''
if node is None:
return
self.inorder(node.lchid)
print(node.elem, end=' ')
self.inorder(node.rchild)
后序遍历:在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点
左子树->右子树->根节点
def postorder(self, node):
'''后序遍历'''
if node is None:
return
self.postorder(node.lchid)
self.postorder(node.rchild)
print(node.elem, end=' ')
广度优先遍历(层次遍历):从树的root开始,从上到下从从左到右遍历整个树的节点
def breadth_travel(self):
'''广度遍历'''
if self.root is None:
return
queue = [self.root]
while queue:
cur_node = queue.pop(0)
print(cur_node.elem, end=' ')
if cur_node.lchid is not None:
queue.append(cur_node.lchid)
if cur_node.rchild is not None:
queue.append(cur_node.rchild)