python几种排序算法和二分查找方法的实现
一.算法概念
- 含义:算法就是对问题进行处理且求解的一种实现思路或者思想。
评判程序优劣的方法
- 消耗计算机资源和执行效率(无法直观)
计算算法执行的耗时(不推荐,因为会受机器和执行环境的影响)
时间复杂度(推荐)
时间复杂度
- 评判规则:量化算法执行的操作/执行步骤的数量,
- 如下列
def sumOfN(n):
# 执行一步:
theSum = 0
for i in range(1,n+1):
# 下式一共执行n步,for循环不算一步,因为它是控制循环次数的
theSum = theSum + i
# return又执行一步
return theSum
# 调用函数,计算执行步骤为:n+2
sumOfN(10)
- 最重要的项:时间复杂度表达式中最有意义的项
- 使用大O记法来表示时间复杂度
O(最重要的项)
# 如上式:最有意义的项就是n,所以n+2 ==>O(n)
常见的时间复杂度:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
数据结构
- 概念:
对于数据(基本类型的数据(int,float,char))的组织方式就被称作为数据结构。数据结构解决的就是一组数据如何进行保存,保存形式是怎样的。
- 使用不同的形式组织数据,在基于查询时的时间复杂度是不一样的。因此认为算法是为了解决实际问题而设计的,数据结构是算法需要处理问题的载体。
平均耗时
- timeit模块:该模块可以用来测试一段python代码的执行速度/时长。
- Timer类:该类是timeit模块中专门用于测量python代码的执行速度/时长的。原型为:class -
- timeit.Timer(stmt='pass',setup='pass')。
stmt参数:表示即将进行测试的代码块语句。
setup:运行代码块语句时所需要的设置。
timeit函数:timeit.Timer.timeit(number=100000),该函数返回代码块语句执行number次的平均耗时。
例子:
from timeit import Timer
def func():
a = []
for i in range(10000):
a.append(i)
return a
def func1():
a = [i for i in range(10000)]
return a
if __name__ == '__main__':
# 计算func1的平均耗时
time_func = Timer("func1()","from __main__ import func1")
t1 = time_func.timeit(1000)
print(t1)
二.栈与队列
1.栈
定义与方法
- 特性:先进后出的数据结构,分栈顶和栈尾
- 相关的方法(待使用列表实现)
Stack() 创建一个空的新栈。 它不需要参数,并返回一个空栈。
push(item)将一个新项添加到栈的顶部。它需要 item 做参数并不返回任何内容。
pop() 从栈中删除顶部项。它不需要参数并返回 item 。栈被修改。
peek() 从栈返回顶部项,但不会删除它。不需要参数。 不修改栈。
isEmpty() 测试栈是否为空。不需要参数,并返回布尔值。
size() 返回栈中的 item 数量。不需要参数,并返回一个整数。
方法的实现(列表)
class Stack(object):
def __init__(self):
self.items = []
# 栈顶添加
def push(self,item):
self.items.append(item)
# 栈顶删除并返回元素
def pop(self):
return self.items.pop()
# 返回栈顶元素索引
def peek(self):
return len(self.items) - 1
# 判断栈是否为空
def isEmpty(self):
return self.items == []
# 返回栈的总长度
def size(self):
return len(self.items)
2.队列
单端队列
定义与方法
- 特性:先进先出
- 方法:
Queue() 创建一个空的新队列。 它不需要参数,并返回一个空队列。
enqueue(item) 将新项添加到队尾。 它需要 item 作为参数,并不返回任何内容。
dequeue() 从队首移除项。它不需要参数并返回 item。 队列被修改。
isEmpty() 查看队列是否为空。它不需要参数,并返回布尔值。
size() 返回队列中的项数。它不需要参数,并返回一个整数。
方法的实现
class Queue():
def __init__(self):
self.items = []
# 新添加的项添加至队尾
def enqueue(self,item):
self.items.insert(0,item)
# 删除队首元素
def dequeue(self):
return self.items.pop()
# 判断队列是否为空
def isEmpty(self):
return self.items == []
#查看队列长度
def size(self):
return len(self.items)
应用案例:烫手山芋
'''
案例:烫手的山芋
烫手山芋游戏介绍:6个孩子围城一个圈,排列顺序孩子们自己指定。第一个孩子手里有一个烫手的山芋,需要在计时器计时1秒后将山芋传递给下一个孩子,依次类推。规则是,在计时器每计时7秒时,手里有山芋的孩子退出游戏。该游戏直到剩下一个孩子时结束,最后剩下的孩子获胜。请使用队列实现该游戏策略,排在第几个位置最终会获胜。
准则:手里有山芋的孩子永远排在队列的头部
'''
# 代码实现
kids = ['A','B','C','D','E','F']
queue = Queue()
for kid in kids:
queue.enqueue(kid) #A对头F队尾
while queue.size() > 1:
for i in range(6): #每循环一次,山芋传递一次,手里有山芋的孩子永远在对头位置
kid = queue.dequeue()
queue.enqueue(kid)
queue.dequeue()
print('获胜的选手是:',queue.dequeue())
双端队列
- 同同列相比,有两个头部和尾部。可以在双端进行数据的插入和删除,提供了单数据结构中栈和队列的特性
- 方法:
Deque() 创建一个空的新 deque。它不需要参数,并返回空的 deque。
addFront(item) 将一个新项添加到 deque 的首部。它需要 item 参数 并不返回任何内容。
addRear(item) 将一个新项添加到 deque 的尾部。它需要 item 参数并不返回任何内容。
removeFront() 从 deque 中删除首项。它不需要参数并返回 item。deque 被修改。
removeRear() 从 deque 中删除尾项。它不需要参数并返回 item。deque 被修改。
isEmpty() 测试 deque 是否为空。它不需要参数,并返回布尔值。
size() 返回 deque 中的项数。它不需要参数,并返回一个整数。
实现方法:
class Deque():
def __init__(self):
self.items = []
def addFront(self,item):
self.items.insert(0,item)
def addRear(self,item):
self.items.append(item)
def removeFront(self):
return self.items.pop()
def removeRear(self):
return self.items.pop(0)
def isEmpty(self):
return self.items == []
def size(self):
return len(self.items)
应用案列:回文检查
def isHuiWen(s):
ex = True
# 实例化
q = Deque()
# 循环添加元素至双端队列中
for ch in s:
q.addFront(ch)
while q.size() > 1:
# 判断从后面删除的的元素是否等于从前方删除的元素
if q.removeFront() != q.removeRear():
ex = False
break
return ex
三.内存相关概念
计算机的作用
- 用来存储和运算二进制的数据
计算机如何计算1+2?
- 将1和2的二进制类型的数据加载到计算机的内存中,然后使用寄存器进行数值的预算。
变量的概念
- 变量就是某一块内存
- 引用:变量==》内存空间的地址
- 指向:如果变量或者引用表示的是某一块内存空间地址的话,则该变量或者该引用指向了该块内存
- 内存空间是有两个默认的属性:
- 内存空间的大小
- bit(位):一个bit大小的内存空间只能存放一位二进制的数
- byte(字节):8bit
- kb:1024byte
- 内存空间的地址
- 使用一个十六进制的数值表示
- 作用:让cup寻址
- 内存空间的大小
四.顺序表和链表
1.顺序表
- 集合中存储的元素是有顺序的,顺序表的结构可以分为两种形式:单数据类型和多数据类型
- python中的列表和元组就属于多数据类型的顺序表(多数据类型顺序表的内存图(内存非连续开辟))
- 缺点***:
顺序表的结构需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁。
2.链表
定义与方法:
- 链表是一种线性表,但是不像顺序表一样连续存储数据,而是每一个结点(数据存储单元)里存放下一个结点的信息(即地址)
- 方法:
. is_empty():链表是否为空
. length():链表长度
. travel():遍历整个链表
. add(item):链表头部添加元素
. append(item):链表尾部添加元素
. insert(pos, item):指定位置添加元素
. remove(item):删除节点
. search(item):查找节点是否存在
实现方法:
# 定义节点类
class Node():
def __init__(self,item):
self.item = item # item用来存值
self.next = None # next用来指向下一个节点内存地址
# 定义一个链表类
class Link():
# 构造出一个空链表
def __init__(self):
#_head存储的只能是空或者第一个节点的地址
self._head = None
# 向链表的头部插入一个节点
def add(self,item):
#创建一个新的节点
node = Node(item)
# 新节点的next指向原头部节点指向的内存地址
node.next = self._head
# 头部节点指向新创建的node的内存地址
self._head = node
# 遍历整个链表
def travel(self):
#_head在链表创建好之后一定是不可变
cur = self._head
while cur:
print(cur.item)
cur = cur.next
def isEmpty(self):
return self._head == None
# 获取链表的长度
def size(self):
cur = self._head
count = 0
while cur:
count += 1
cur = cur.next
return count
# 向链表的尾部添加元素
def append(self,item):
node = Node(item)
#特殊情况
if self._head == None:
self._head = node
return
cur = self._head
pre = None# pre指向的是cur前一个节点
while cur:
pre = cur
cur = cur.next
pre.next = node
# 查找某个值是否存在于链表中
def search(self,item):
find = False
cur = self._head
while cur:
if cur.item == item:
find = True
break
cur = cur.next
return find
# 向某个位置插入
def insert(self,pos,item):
node = Node(item)
pre = None
cur = self._head
for i in range(pos):
pre = cur
cur = cur.next
pre.next = node
node.next = cur
# 删除某个值对应的节点
def remove(self,item):
cur = self._head
pre = None
#删除的是第一个节点
if cur.item == item:
self._head = cur.next
return
while cur:
'''
如果当前节点的值与所搜寻的值相等,则前一节点的next指向当前节点的next属性指向的内存地址
'''
pre = cur
cur = cur.next
if cur.item == item:
pre.next = cur.next
return
if cur.next == None:
return
五.排序方法和查找方法
1.查找方法
顺序查找
- 定义:
当数据存储在诸如列表的集合中时,我们说这些数据具有线性或顺序关系。 每个数据元素都存储在相对于其他数据元素的位置。 由于这些索引值是有序的,我们可以按顺序访问它们。 这个过程产实现的搜索即为顺序查找。
- 顺序查找原理剖析:
从列表中的第一个元素开始,我们按照基本的顺序排序,简单地从一个元素移动到另一个元素,直到找到我们正在寻找的元素或遍历完整个列表。如果我们遍历完整个列表,则说明正在搜索的元素不存在。
- 代码实现:
该函数需要一个列表和我们正在寻找的元素作为参数,并返回一个是否存在的布尔值。found 布尔变量初始化为 False,如果我们发现列表中的元素,则赋值为 True。
二分查找
前提:
- 一定是基于有序集合的查找!!!
- 有序列表对于我们的实现搜索是很有用的。在顺序查找中,当我们与第一个元素进行比较时,如果第一个元素不是我们要查找的,则最多还有 n-1 个元素需要进行比较。 二分查找则是从中间元素开始,而不是按顺序查找列表。 如果该元素是我们正在寻找的元素,我们就完成了查找。 如果它不是,我们可以使用列表的有序性质来消除剩余元素的一半。如果我们正在查找的元素大于中间元素,就可以消除中间元素以及比中间元素小的一半元素。如果该元素在列表中,肯定在大的那半部分。然后我们可以用大的半部分重复该过程,继续从中间元素开始,将其与我们正在寻找的内容进行比较。
代码实现:
# 二分查找 [1,2,3,4,5,6,7,8,9,10,27,36,46,58,69] - 有序列表
l1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 27, 36, 46, 58, 69, 100]
def func(lis, n):
count = 0 # 定义查找次数
left = 0 # 定义左边界
right = len(lis) - 1 # 定义右边界
# 循环查找,循环条件 左边界 <= 右边界
while left <= right:
# 二分:中间元素位置
num = (left + right) // 2
# 如果查找的元素大于左面的最后一个元素,左边界变为 二分位置+1
if n > lis[num]:
left = num + 1
# 如果查找的元素大于左面的最后一个元素,右边界变为 二分位置-1
elif n < lis[num]:
right = num - 1
# 直到找到为止
else:
msg = f'位置为{num},查找了{count}次'
return msg
count += 1
print(func(l1, 5))
2.七种排序方法
①.有序链表:
原理:
- 实现链表的有序插入
实现方法:
# 定义节点类
class Node():
def __init__(self,item):
self.item = item # item用来存值
self.next = None # next用来指向下一个节点内存地址
# 定义有序链表
class SortLink():
def __init__(self):
self._head = None
'''基础方法见4.顺序表和链表'''
# 链表的有序插入
def sortInsert(self,item):
node = Node(item)
#插入的是第一个节点
if self._head == None:
self._head = node
return
cur = self._head
pre = None
# 当item比第一个节点的值还小时,插入到第一个节点位置
if item < cur.item:
self._head = node
node.next = cur
return
# 循环比较当前节点的值与item的大小关系
while cur:
pre = cur
cur = cur.next
# 如果cur后没有节点了,则把node放在最后的位置
if cur == None:
pre.next = node
return
# 找到当前节点值比item大的节点,把node插在当前节点前
if cur.item > item:
pre.next = node
node.next = cur
return
②.二叉树排序
二叉树相关概念
- 根节点
- 叶子节点:左叶子节点,右叶子节点
- 树的层级,树的高度
- 二叉树的遍历
1.广度优先遍历
一层一层对节点进行遍历
2.深度优先遍历
前序:根左右
中序:左根右
后序:左右根
二叉树实现方法:
类似于链表.
# 定义节点类
class Node():
def __init__(self,item):
self.item = item
self.left = None # 左叶子节点
self.right = None # 右叶子节点
# 定义二叉树类
class Tree():
def __init__(self):
self.root = None
def addNode(self,item):
node = Node(item)
#如果插入第一个节点的情况
if self.root == None:
self.root = node
return
cur = self.root
q = [cur] #列表元素是我们进行遍历判断的节点
while q:
nd = q.pop(0)
if nd.left == None:
nd.left = node
return
else:
q.append(nd.left)
if nd.right == None:
nd.right = node
return
else:
q.append(nd.right)
def travel(self):
cur = self.root
q = [cur]
while q:
nd = q.pop(0)
print(nd.item)
if nd.left:
q.append(nd.left)
if nd.right:
q.append(nd.right)
def forwoar(self,root):#前序:根左右
if root == None:
return
print(root.item)
self.forwoar(root.left)
self.forwoar(root.right)
def middle(self,root):#前序:左根右
if root == None:
return
self.middle(root.left)
print(root.item)
self.middle(root.right)
def back(self,root):#前序:左右根
if root == None:
return
self.back(root.left)
self.back(root.right)
print(root.item)
# 使用:
tree = Tree()
tree.addNode(1)
tree.addNode(2)
tree.addNode(3)
tree.addNode(4)
tree.addNode(5)
tree.addNode(6)
tree.addNode(7)
# tree.travel()
# tree.forwoar(tree.root)
# tree.back(tree.root)
排序二叉树实现方法:
# 定义节点类
class Node():
def __init__(self,item):
self.item = item
self.left = None
self.right = None
# 定义排序二叉树类
class SortTree():
def __init__(self):
self.root = None
# 重点:添加新节点
def add(self,item):
node = Node(item)
cur = self.root
# 二叉树还没有节点的情况
if self.root == None:
self.root = node
return
'''
二叉树有节点时循环比较当前节点的值与需要插入的节点的值的大小:
1.比当前节点值大,向右侧插入,并判断当前右侧是否为空,不是则cur = cur.right
2.比当前节点值小,向左侧插入,并判断当前左侧是否为空,不是则cur = cur.left
3.循环直到找到左侧或右侧满足条件的某个空位置结束,调出循环
'''
while cur:
#向右侧插入
if item > cur.item:
if cur.right == None:
cur.right = node
break
else:
cur = cur.right
else:#向左插入
if cur.left == None:
cur.left = node
break
else:
cur = cur.left
# 使用中序的方法来获取有序二叉树的各个节点的值
def middle(self,root):#前序:左根右
if root == None:
return
self.middle(root.left)
print(root.item)
self.middle(root.right)
③.冒泡排序
原理:
- 相邻元素两两比较,大的元素往后放,第一完毕后,最大值就在最大索引处。然后再继续第二大……,这样就可得到一个排好序的数组了。
实现方法:
#逐渐将乱序序列的最大值找出放置在乱序序列的尾部
def sort(alist):
# 外层循环根据列表长度控制循环次数
for j in range(len(alist)-1):
# 内层循环根据当前外层循环次数来控制需要比较的次数,即第j大的元素
for i in range(len(alist)-1-j):
# 俩俩比较,大小互换位置
if alist[i] > alist[i+1]:
alist[i],alist[i+1] = alist[i+1],alist[i]
return alist
alist = [3,8,5,7,6]
print(sort(alist))
④.选择排序
原理:
- 选择排序改进了冒泡排序,每次遍历列表只做一次交换。为了做到这一点,一个选择排序在他遍历时寻找最大的值,并在完成遍历后,将其放置在正确的位置。
- 对于冒泡排序来讲选择排序由于交换数量的减少,选择排序通常在基准研究中执行得更快。
代码实现:
# 例如:
alist = [1,3,8,5,2]
def maopao(alist):
# 外层循环根据列表长度控制循环冒泡进行次数
for j in range(0,len(alist)-1):
max_index = 0 # 当前循环最大元素的索引
# 里层循环找出第j大的元素的索引,并赋值给max_index
for i in range(1,len(alist)-j):
# 俩俩比较
if alist[i-1] < alist[i]:
max_index = i
# 互换当前循环最大值元素与列表len(alist)-1-j元素位置
alist[max_index],alist[len(alist)-1-j] = alist[len(alist)-1-j],alist[max_index]
return alist
⑤.插入排序
原理:
- 一种特殊的希尔排序
- 插入排序的主要思想是每次取一个列表元素与列表中已经排序好的列表段进行比较,然后插入从而得到新的排序好的列表段,最终获得排序好的列表。比如,待排序列表为[49,38,65,97,76,13,27,49],则比较的步骤和得到的新列表如下:(带有背景颜色的列表段是已经排序好的,红色背景标记的是执行插入并且进行过交换的元素)
实现方法:
alist = [1,3,8,5,2]
def insert_func(alist):
# 外层循环控制alist的索引
for i in range(1,len(alist)):
# 循环判断当前索引对应的元素与前面已排好序的元素的大小关系
while i > 0:
# 如果小于前一位元素,则互换位置,索引-1,继续比较,直至索引为1
if alist[i] < alist[i-1]:
alist[i],alist[i-1] = alist[i-1],alist[i]
i -= 1
else:
# 如果大于前一位元素,说明不用动地方。跳出循环,判断下一个
break
return alist
⑥.希尔排序
原理:
- 希尔排序(Shell Sort)也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本,该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量(gap)”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率比直接插入排序有较大提高
- gap:增量值,拆分出来各个组数
- 插入排序就是增量为1的希尔排序
实现方法:
def sort(alist):
gap = len(alist)//2 # gap第一次设置为总长度为2
# 当gap的值大于等于1的时候
# 根据插入排序的原理给各个gap下分出的数组排序
while gap >= 1:
for i in range(gap,len(alist)):
while i > 0:
if alist[i] < alist[i-gap]:
alist[i],alist[i-gap] = alist[i-gap],alist[i]
i -= gap
else:
break
# 排序完当前下根据gap的分组,gap = gap // 2
gap //= 2
return alist
⑦.快速排序
原理:
- 将列表中第一个元素设定为基准数字,赋值给mid变量,然后将整个列表中比基准小的数值放在基准的左侧,比基准到的数字放在基准右侧。然后将基准数字左右两侧的序列在根据此方法进行排放。
- 定义两个指针,low指向最左侧,high指向最右侧
- 然后对最右侧指针进行向左移动,移动法则是,如果指针指向的数值比基准小,则将指针指向的数字移动到基准数字原始的位置,否则继续移动指针。
- 如果最右侧指针指向的数值移动到基准位置时,开始移动最左侧指针,将其向右移动,如果该指针指向的数值大于基准则将该数值移动到最右侧指针指向的位置,然后停止移动。
- 如果左右侧指针重复则,将基准放入左右指针重复的位置,则基准左侧为比其小的数值,右侧为比其大的数值。
实现方法:
def sort(alist,start,end):
low = start # 左侧指针
high = end # 右侧指针
#递归结束的条件
if low > high:
return
# 基准:最左侧的数值
mid = alist[low]
# low和high的关系只能是小于,当等于的时候就要填充mid了
while low < high:
'''
从右侧开始判断:
如果当前high指针指向的元素值大于mid基准值,则high指针向左偏移1位,即high-1,继续循环;
如果当前high指针指向的元素值小于mid基准值,则将high指针指向的元素赋予至low指针指向的位置,结束当前循环,退出右侧循环
从左侧开始判断:
如果当前low指针指向的元素值小于mid基准值,则high指针向右偏移1位,即high+1,继续循环;
如果当前low指针指向的元素值大于mid基准值,则将low指针指向的元素赋予到high指针指向的位置,结束当前循环,退出左侧循环
判断当前的low指针是否与high指针重合,如果重合,将mid的基值放在low或者high的位置,如果不等,继续下一次循环.
'''
while low < high:
if alist[high] > mid:
high -= 1
else:
alist[low] = alist[high]
break
while low < high:
if alist[low] < mid:
low += 1
else:
alist[high] = alist[low]
break
#当low和high重复的时候,将mid填充
if low == high:
alist[low] = mid #or alist[high] = mid
break
'''
按照low指针与high指针重合的位置切分列表,递归调用本体函数对切分后的列表进行快排!!
'''
# 快排切分后的左侧序列
sort(alist,start,high-1)
# 快排切分后的右侧序列
sort(alist,low+1,end)
return alist
alist = [6,1 , 2, 7, 9, 3, 4, 5, 10, 8]
六.常见问题(面试题)
如何使用两个队列实现一个栈?
实现方法:
# 定义一个队列类
class Queue():
def __init__(self):
self.items = []
def enqueue(self,item):
self.items.insert(0,item)
def dequeue(self):
return self.items.pop()
def size(self):
return len(self.items)
# 数据准备
q1 = Queue()
q2 = Queue()
for i in [1,2,3,4,5]: q1.append(i)
'''
思路:
1.首先了解:栈先进后出,队列先进先出
2.取值时,将q1前n-1项取出存在q2中,将最后一个值打印出来
'''
# 按q1的长度来控制循环次数
while q1.size() > 0:
# 取出前q1的前n-1项,放置在q2中
while q1.size() > 1:
item = q1.dequeue()
q2.append(item)
print(q1.dequeue())
# 此时q1位空,q2为原q1的前n-1项,将q1,q2互换,然后进行下次循环
q1,q2 = q2,q1
如何实现将单链表倒置?
实现方法:
class Node():
def __init__(self,item):
self.item = item
self.next = None
class Link():
def __init__(self):
self._head = None
def append(self,item):
node = Node(item)
if self._head == None:
self._head = node
return
cur = self._head
pre = None
while cur:
pre = cur
cur = cur.next
pre.next = node
def travel(self):
cur = self._head
while cur:
print(cur.item)
cur = cur.next
def remove(self,item):
cur = self._head
pre = None
#删除的是第一个节点
if cur.item == item:
self._head = cur.next
return
while cur:
pre = cur
cur = cur.next
if item == cur.item:
pre.next = cur.next
return
'''
实现倒置单链表思路:
1.主要就是改变节点的指向,当前节点指向前一节点,当前节点的后一节点指向当前节点.
2.self._head的指向原链表的最后一个节点
3.所以实现此方法需要三个变量:pre 前一节点,cur 当前节点, next_node 下一节点
'''
def reverse(self):
cur = self._head # 未循环当前节点为self._head
pre = None # 前一节点为None
next_node = cur.next # 下一节点为cru.next
# 开始循环倒置链表
while cur:
# 改变当前节点的指向,使其指向前一节点pre
cur.next = pre
# 节点往后偏移,pre = cur, cur = next_node,
pre = cur
cur = next_node
# 如果cur不为None时,next_node = cur.next
if cur:
next_node = cur.next
# 当cur为空时,结束循环,所有的节点指向已倒置完毕,改变self._head的指向
# 注意此时应该指向pre而不是cur!
self._head = pre