Python中的数据结构和算法

时间:2022-11-21 06:10:16

一、算法

1.算法的时间复杂度

大 O 记法,是描述算法复杂度的符号
O(1)
  常数复杂度,最快速的算法。
  取数组第 1000000 个元素
  字典和集合的存取都是 O(1)
  数组的存取是 O(1)

O(logN)
  对数复杂度
  假设有一个有序数组,以二分法查找

O(n)
  线性复杂度
  假设有一个数组,以遍历的方式在其中查找元素 最坏情况是全部过一遍

O(nlogn)
  求两个数组交集,其中一个是有序数组
  A 数组每一个元素都要在 B 数组中进行查找操作
  每次查找如果使用二分法则复杂度是 logN

O(N^2)
  平方复杂度
  求两个无序数组的交集

2.常见问题
"""
常见问题:
O(1)和O(n)区别是什么?
O(1) 是确定的
O(n) 是随着数据规模增长而增长的 最坏情况是全部过一遍 如果第一次就找到了 还是O(n)吗?
我们衡量的是性质 不是特例 以最坏情况来计算 复杂度就意味着查询时间的多少吗? 常数复杂度是直接取第一百万个元素还是按顺序来?
程序中循环的次数,而非真正意义上的时间,因为不同的机器可能执行相同的程序所耗时不同
常数复杂度是说 执行的时间 是确定的 数组的长度不一样,存取元素的时间复杂度是不一样的吗?
数组是一个随机存取的数据结构
无论多长, 存取元素的时间都是确定的, O(1)
"""
# 都是O(1)
# def fun1():
# for i in range(1000000):
# print(i)
# for i in range(1000000):
# print(i*2)
#
# def fun2():
# for i in range(1000000):
# print(i)
# print(i*2)

二、数据结构基础

1.数组

  连续的一块内存,存取元素时间是 O(1),插入、删除是 O(n)

2.链表

  像火车,车厢挂车厢

  以下标方式读取元素的时间是 O(n)

  插入、删除是 O(1)

  栈和队列是链表的改良型

  栈:先进后出(压弹夹)

  队列:先进先出(银行取号排队)

class Node():
def __init__(self, element=None):
self.e = element
self.next = None head = Node()
n1 = Node(111)
n2 = Node(222)
n3 = Node(333)
n1.next = n2
n2.next = n3 head.next = n1 def append(node, element):
"""
我们往 node 的末尾插入一个元素
:param node: 是一个 Node 实例
:param element: 任意类型的元素
"""
n = node
# 这里不应该使用 while n.next: 这样的隐含类型转换来判定
while n.next is not None:
n = n.next
# n 现在是最后一个元素
new_node = Node(element)
n.next = new_node def prepend(head, element):
n = Node(element)
n.next = head.next
head.next = n def pop(head):
"""
pop 是 stack 的两个操作之一
push 入栈
pop 出栈 现在我们有这么 3 个函数
append 添加一个元素到末尾
prepend 添加一个元素到开头
pop 删除并返回末尾的元素 prepend + pop 就实现了 队列 的 入队(enqueue)和出队(dequeue)操作
append + pop 就实现了 栈 的 入栈(push) 和 出栈(pop)操作
"""
tail = head
while tail.next is not None:
tail = tail.next
# 现在 tail 是最后一个元素了
n = head
while n.next is not tail:
n = n.next
# 现在 n 是 tail 之前的元素了
n.next = None
return tail.e def log_list(node):
n = node
s = ''
while n is not None:
s += (str(n.e) + ' > ')
n = n.next
print(s) # log_list(n1)
# append(n1, 'gua')
# log_list(n1)
# prepend(head, 'hello')
# log_list(head)
# print('pop head ', pop(head))
# log_list(head)

链表

"""

队列的特点是「先进先出」,一般有这几个操作

# enqueue 将一个元素存入队列中
# dequeue 将一个元素从队列中取出,并在队列中删除它 # empty 查看栈是否是空的 可以把队列看做排队,银行叫号机就是队列,先取号的先入队,叫号的时候也就先出队 """ # Node类是一个节点,有两个属性,一个存储元素,一个存储指向另一个节点的引用
class Node():
def __init__(self, element=None, next=None):
self.element = element
self.next = next # 这个函数是会在print的时候被自动调用,就是把这个Node显示出来
def __repr__(self):
return str(self.element) class Myqueue():
# 初始化函数,自动被调用
# 初始化Queue()类的时候,它有2个属性,分别指向头尾
def __init__(self):
self.head = Node()
self.tail = self.head # 如果head的next属性为空,则说明队列是空的
def empty(self):
return self.head.next is None # 创建一个node
# 让tail.next指向它
# 让tail指向它,tail现在就是新的队尾了
def enqueue(self, element):
n = Node(element)
self.tail.next = n
self.tail = n # 取出head.next指向的元素,如果队列不是空的,就让head.next指向node.next,这样node就不在队列中了
def dequeue(self):
node = self.head.next
if not self.empty():
self.head.next = node.next
return node # 测试函数
def test():
q = Myqueue() q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
q.enqueue(4) print(q.dequeue())
print(q.dequeue())
print(q.dequeue())
print(q.dequeue()) if __name__ == '__main__':
# 运行测试函数
test()

Myqueue

"""
数据结构的核心是,一个结构只有特定的操作,可以实现特定的功能 栈的特点是「先进后出」,一般有这几个操作
# push 将一个元素存入栈中
# pop 将一个元素从栈中取出,并在栈中删除它 # top 将一个元素从栈中取出
# is_empty 查看栈是否是空的 """ # Node类是一个节点,有两个属性,一个存储元素,一个存储指向另一个节点的引用
class Node():
def __init__(self, element=None, next=None):
self.element = element
self.next = next # 这个函数是会在print的时候被自动调用,就是把这个Node显示出来
def __repr__(self):
return str(self.element) class Mystack():
# 初始化函数,自动被调用
# 初始化Stack()类的时候,它有一个head属性,值是一个空的Node
def __init__(self):
self.head = Node() # 如果head的next属性为空,则说明栈是空的
def is_empty(self):
return self.head.next is None # 创建一个node,并让它指向当前head.next指向的元素,再把head.next指向它
def push(self, element):
self.head.next = Node(element, self.head.next) # 取出head.next指向的元素,如果栈不是空的,就让head.next指向node.next,这样node就不在栈中了
def pop(self):
node = self.head.next
if not self.is_empty():
self.head.next = node.next
return node # head.next就是栈里面第一个元素
def top(self):
return self.head.next # 测试函数
def test():
s = Mystack() s.push(1)
s.push(2)
s.push(3)
s.push(4) print(s.pop())
print(s.pop())
print(s.pop())
print(s.pop()) if __name__ == '__main__':
# 运行测试函数
test()

Mystack

3.字典(哈希表 对象 Map)

  把字符串转为数字作为下标存储到数组中

  字符串转化为数字的算法是 O(1)

  所以字典的存取操作都是 O(1)

  除非对数据有顺序要求,否则字典永远是最佳选择

  字符串转化为数字的算法

    1,确定数据规模,这样可以确定容器数组的大小 Size

    2,把字符当作 N 进制数字得到结果

      'qwe' 被视为 q* 1 + w * 10 + e* 100 得到结果 n

      n % Size 作为字符串在数组中的下标

      通常 Size 会选一个 素数

    3, 当下标冲突(冲突属于叫做碰撞)的时候, 我们有标准解决碰撞的办法

      链接法

class HashTable(object):
def __init__(self):
# table 是用来存储数据的数组
# 先让它有 10007 个格子好了
# 上课的时候说过, 这个尺寸最好选素数
# 这样可以得到更为合理的下标分布
self.table_size = 10007
self.table = [0] * self.table_size # 这个魔法方法是用来实现 in not in 语法的
def __contains__(self, item):
return self.has_key(item) def has_key(self, key):
"""
检查一个 key 是否存在, 时间很短, 是 O(1)
如果用 list 来存储, 需要遍历, 时间是 O(n)
"""
index = self._index(key)
# 取元素
v = self.table[index]
if isinstance(v, list):
# 检查是否包含我们要找的 key
for kv in v:
if kv[0] == key:
return True
# 如果得到的是 int 0 说明没找到, 返回 False
# 如果得到的是 list 但是遍历结果没有我们要找的 key 也是没找到
return False def _insert_at_index(self, index, key, value):
# 检查下标处是否是第一次插入数据
v = self.table[index]
data = [key, value]
# 也可以用这个判断 if v == 0:
if isinstance(v, int):
# 如果是第一次, 得到的是 int 0
# 那么就插入一个 list 来存, 以后相同 key 的元素都放这里面
# 注意我们把 key value 作为一个数组保存进去了, 这是因为
# 会出现相同 hash 值的 key
# 这时候就需要比较原始信息来找到相应的数据
self.table[index] = [data]
else:
# 如果不是, 得到的会是 list, 直接 append
self.table[index].append(data) def add(self, key, value):
"""
add 函数往 hashtable 中加入一对元素
我们先只支持字符串当 key; value是uuid生成的随机字符串
"""
# 先计算出下标
index = self._index(key)
# 在下标处插入元素
self._insert_at_index(index, key, value) def get(self, key, default_value=None):
"""
这个和 dict 的 get 函数一样
"""
index = self._index(key)
# 取元素
v = self.table[index]
if isinstance(v, list):
# 检查是否包含我们要找的 key
for kv in v:
if kv[0] == key:
return kv[1]
# 如果得到的是 int 0 说明没找到, 返回 default_value
# 如果得到的是 list 但是遍历结果没有我们要找的 key 也是没找到
return default_value def _index(self, key):
# 先计算出下标
return self._hash(key) % self.table_size def _hash(self, s):
"""
# 输入一个字符串,生成一串对应的数字
下划线开始的函数被我们视为私有函数
但实际上还是可以在外部调用, 这只是一个给自己看的标记
"""
n = 1
f = 1
for i in s:
n += ord(i) * f
f *= 10
return n def test():
import uuid
names = [
'gua',
'xiao',
'name',
'web',
'python',
]
ht = HashTable()
for key in names:
value = uuid.uuid4()
ht.add(key, value)
print('add 元素', key, value)
for key in names:
v = ht.get(key)
print('get 元素', key, v)
print('魔法方法', 'gua' in ht) if __name__ == '__main__':
test()

hash

4.搜索树
class Tree(object):
def __init__(self, element=None):
self.element = element
self.left = None
self.right = None def traversal(self):
"""
树的遍历, 是一个递归操作
"""
print(self.element)
if self.left is not None:
self.left.traversal()
if self.right is not None:
self.right.traversal() def reverse(self):
self.left, self.right = self.right, self.left
if self.left is not None:
self.left.reverse()
if self.right is not None:
self.right.reverse() def test():
# 手动构建二叉树
# 为什么手动这么麻烦呢, 因为一般都是自动生成的
# 这里只需要掌握性质就好
t = Tree(0)
left = Tree(1)
right = Tree(2)
t.left = left
t.right = right
# 遍历
t.traversal() if __name__ == '__main__':
test()

tree