数据的存储和组织形式
程序 = 数据结构 + 算法
一. 算法介绍
概述目的
都是可以提高程序的效率(性能), 面试高频考点
数据结构介绍
数据的存储和组织形式, 同样的空间, 不同的结构, 存储的数据不同, 操作方式也不同
算法介绍
为了解决实际的业务问题, 而考虑出来的方法和思路 => 算法
算法具有独立性, 即: 他是解决问题的思路和方法, 不依赖于语言, 用C能实现, 用Java能实现, 用python也能实现
算法5大特性
-
有输入 算法具有0个或者多个输入
-
有输出 算法至少有1个或多个输出
-
有穷性 算法在有限的步骤之后会自动结束而不会死循环, 并且每个步骤都在可接受的时间内完成
-
确定性 算法中的每一步都具有确定的含义, 不会出现二义性
-
可执行 算法的每一步都是可行的, 执行有限的次数完成
案例
经典案例: a + b + c = 1000, a^2 + b^2 = c^2
方式1: 穷举法, 155秒
import time start = time.time() for a in range(1001): for b in range(1001): for c in range(1001): if a ** 2 + b ** 2 == c ** 2 and a + b + c == 1000: print(a, b, c) end = time.time() print(f'执行了: {end - start}')
方式2: 穷举法, 88.9秒
import time start = time.time() for a in range(1001): for b in range(1001): for c in range(1001): if a + b + c == 1000 and a ** 2 + b ** 2 == c ** 2: print(a, b, c) end = time.time() print(f'执行了: {end - start}')
方式3: 代入法(从a, b可得c, 减少一层循环), 0.236秒
import time start = time.time() for a in range(1001): for b in range(1001): c = 1000 - a - b if a ** 2 + b ** 2 == c ** 2: print(a, b, c) end = time.time() print(f'执行了: {end - start}')
衡量算法的优略
不能单纯的只依靠程序的执行时间, 因为程序执行时间也会收到机器, 硬件, 其他硬件的影响.
所以可以假定 计算机执行每个步骤的时间都是固定的, 只考虑算法的 执行步骤即可.
代码执行总时间(T) = 操作步骤数量 * 操作步骤执行时间
时间复杂度
时间复杂度T 是关于n的函数
概述
可以反应一个算法的优略, 他表示一个算法随着问题规模的变化而表现出来的主要趋势.
大O标记法
忽略次要条件, 只考虑主要条件(随着问题规模变化而变化的内容), 就会得到: 大O标记法, 标记的效率
例如: 4中 方式1穷举法 的时间复杂度为 : O(n³)
例如: 4中 方式3代入法 的时间复杂度为 : O(n²)
时间复杂度计算
-
基本操作: O(1) => 固定步骤
-
顺序结构: 加法
-
循环结构: 乘法
-
分支结构: 取最大项
-
只考虑主要条件, 不考虑次要条件
细节
-
如非特别说明, 考虑时间复杂度都是考虑: 最坏时间复杂度, 它算是算法的一种保证
-
常见的时间复杂度, 效率从高到低分别是:
O(1) > O(logn) > O(n) > O(nlogn) > O(n²) > O(n³)
空间复杂度(了解)
-
空间复杂度指的是: 算法的运算过程中, 临时占用的空间大小, 也用大O标记法标记, 具体如下:
O(1) < O(logn) < O(n) < O(n²) < O(n³)
-
开发思想: 时空转换, 即: 用空间换空间 / 用空间换时间
-
数据结构是算法的载体
二. 数据结构
线性结构
特点:每个节点都只能有1个子节点(后继节点) 和 1个父节点(前驱节点)
代表:列表, 栈, 列表, 链表...
顺序表
概述: 属于线性结构的一种, 例如: 栈, 队列都属于顺序表
特点: 所有元素在内存中以 连续 的内存空间 来存
分类
一体式存储: 地址和数据一起存储
分离式存储: 地址和数据分离存储
扩容策略
-
每次扩容增加固定的条数目, 拿时间环空间
-
每次扩容容量翻倍, 拿空间换时间
细节:
顺序表主要存储 数据区 和信息区, 数据区和信息区在一起的叫: 一体式存储, 分开存储 的叫: 分离式存储
顺序表增删
方式1: 末尾增删: 时间复杂度: O(1)
方式2: 中间增删(非保序) 时间复杂度: O(1)
方式3: 中间增删(保序) 时间复杂度: O(n)
链表
概述: 他是由节点组成, 每个节点由 数值域 和 地址域 组成
特点: 所有元素在内存中以 非连续 的内存空间来存储, 即: 有地就行
链表介绍
-
概述: 属于线性结构, 即: 每个节点都只能有1个子节点(后继节点) 和 1个父节点(前驱节点)
链表可以看作是 用链条把所有节点连接起来的 一种结构
-
节点概述: 由元素域(数值域) 和地址域(连接域)组成, 其中 数值域存储的是 数值, 地址域存储的是下个节点的地址
分类(根据节点)
-
单向链表:
每个节点由1个数值域和1个地址域组成, 且每个节点的地址域指向下个节点的地址, 最后1个节点的地址域为None
-
单项循环链表:
每个节点由1个数值域和1个地址域组成, 且每个节点的地址域指向下个节点的地址, 最后1个节点的地址域为: 第1个节点的地址
-
双向链表
每个节点由1个数值域和2个地址域组成, 且每个节点的地址域分别指向前1个节点的地址 和 后1个节点的地址.
第1个节点的(前)地址域为: None, 最后1个节点的(后)地址域为: None
-
双项循环链表
每个节点由1个数值域和2个地址域组成, 且每个节点的地址域分别指向前1个节点的地址 和 后1个节点的地址.
第1个节点的(前)地址域为: 最后1个节点的地址,
最后1个节点的(后)地址域为: 第1个节点的地址
单链表演示
分析
节点类: Single Node
属性:
item 表示: 数值域
next 表示: 地址域, 即: 指向下个节点的地址
链表类: SingleLinkedList
属性:
head 表示: 头节点, 即: 默认指向链表第一个节点的地址
行为:
is_empty(self)链表是否为空
length(self)链表长度
travel(self) 遍历整个链表
add(self,item)链表头部添加元素
append(self,item)链表尾部添加元素
insert(self,pos,item)指定位置添加元素
remove(self,item)删除节点
search(self,item)查找节点是否存在
代码
# 定义节点类 class SingleNode(object): # 初始化节点属性 def __init__(self, item): self.item = item self.next = None # 定义链表类 class SingleLinkedList(object): # 初始化链表属性 def __init__(self, head=None): self.head = head # 判断链表是否为空 def is_empty(self): return self.head is None # 链表长度 def length(self): # 定义计数器 count = 0 # 定义当前节点, 初值指向头节点 cur = self.head # 判断当前节点是否为空, 不为空遍历 while cur is not None: # 计数器加1 count += 1 # 设置当前节点指向下个节点 cur = cur.next # 返回链表长度计数器 return count # 遍历链表 def travel(self): # 定义当前节点, 初值指向头节点 cur = self.head # 判断当前节点是否为空, 不为空遍历 while cur is not None: print(cur.item) # 设置当前节点指向下个节点 cur = cur.next # 头插法 def add(self, item): # 将要添加的数据封装成节点 new_node = SingleNode(item) # 设置新节点的地址域为头节点 new_node.next = self.head # 将头节点为新节点 self.head = new_node # 尾插 def append(self, item): # 将要添加的节点封装成节点 new_node = SingleNode(item) # 判断当前头节点是否为空 if self.head is None: # 为空直接将头节点设置为新节点 self.head = new_node else: # 不为空则遍历链表 cur = self.head # 判断当前节点的地址域是否不为空 while cur.next is not None: # 这里是当前节点的地址域不为空, 继续遍历 cur = cur.next # 这里找到最后节点, 将最后节点的地址域设置为新节点 cur.next = new_node # 插入到指定位置(索引从0开始) def insert(self, pos, item): # 判断插入的位置是否合法 if pos <= 0: # 小于0, 则在头部插入 self.add(item) elif pos >= self.length(): # 大于链表长度, 则在尾部插入 self.append(item) else: # 定义计数器 count = 0 # 定义存储当前的节点 cur = self.head # 判断是否为要插入位置的前一个节点 while count < pos - 1: cur = cur.next count += 1 # 具体插入动作 new_node = SingleNode(item) # 顺序不能变 new_node.next = cur.next cur.next = new_node # 删除节点 def remove(self, item): # 当前节点 cur = self.head # 当前节点的前一个节点 pre = None while cur is not None: if cur.item == 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 is not None: if cur.item == item: return True else: cur = cur.next return False if __name__ == '__main__': # 测试节点类 node1 = SingleNode('张三') print(node1) print(node1.item) print(node1.next) print('-' * 21) # 测试创建空链表 linkedlist1 = SingleLinkedList() print(linkedlist1.head) print('-' * 21) # 测试创建链表, 头节点指向node1节点 linkedlist2 = SingleLinkedList(node1) print(linkedlist2.head) print(linkedlist2.head.item) print(linkedlist2.head.next) print('-' * 21) # 测试头添加 linkedlist2.add('李四') linkedlist2.add('王五') # 测试尾部添加 linkedlist2.append('麻子') linkedlist2.append('孙二') # 添加到指定位置 linkedlist2.insert(-10, '-10') linkedlist2.insert(200, '200') linkedlist2.insert(2, '2') # 删除元素 linkedlist2.remove('-10') linkedlist2.remove('2') linkedlist2.remove('200') # 查找 print('查找') print(linkedlist2.search('-2')) print(linkedlist2.search('麻子')) print('-' * 21) # 测试是否为空 print('空') print(linkedlist1.is_empty()) print(linkedlist2.is_empty()) print('-' * 21) # 链表长度 print('长度') print(linkedlist1.length()) print(linkedlist2.length()) print('-' * 21) # 遍历链表 print('遍历') print('') linkedlist1.travel() linkedlist2.travel() # print('-' * 21)
非线性结构
特点: 每个节点都可以有n个子节点(后继节点) 和 n个父节点(前驱节点)
代表: 树, 图......