数据结构与算法初识
前言
前面和大家聊过面向对象及其三大特征等等,感兴趣的朋友可以前往观看。今天简单谈一下数据结构和算法,这块儿可以说是程序员绕不开的两座"浪浪山"。那什么是数据结构?什么是算法?在程序开的中又起到什么作用呢…
首先我们知道一个程序的诞生是为了解决某个问题(需求)。那按照这个思路想可以把编写一个程序比作一场战役。我们写的代码就是战士,程序解决问题的过程就是战斗的过程。那再往下延伸在战役中有指挥作战的元帅就是我们程序员。那战役如何取胜需要指挥者排兵布阵,那“问题来了”数据结构就好比士兵的阵型、阵法。本次战役选择何种战术,如何根据战争的规模与多变性灵活机动地采取不同的战术就显得尤为重要。这里的战术就是程序中的算法。说到这里小伙伴大概可以理解什么是数据结构和算法了吧。有了这个初概念下面我们结合一些例子分段来说明数据结构和算发。Let’s go!
一、数据结构
1.1 概述
编程中数据结构简单理解就是存储和组织数据的方式。
1.2 分类
1.2.1 线性和非线性结构
数据结构中如果单个节点有且只有一个前驱节点和有且只有一个后继节点时这样的被称为线性结构。如果单个节点可以有N个前驱节点和N个后继节点时称这样的数据结构为非线性结构。
1.2.2 顺序表和链表
在线性结构中如果采用连续的内存空间来存储数据就称为顺序表,否则称为链表。
1.2.3 一体式存储和分离式存储
在顺序表存储中如果数值与地址在一起则称为一体式存储,否则称为分离式存储。
1.2.4 链表分类
单向链表、单向循环链表、双向链表、双向循环链表。
1.2.5 二叉树
每个节点最多只能有两个子节点。
1.2.6 完全二叉树
除了最后一层其他层的节点都是两个子节点。
1.2.7 满二叉树
包括最后一层所有层的节点都是两个子节点。
1.2.8 不完全二叉树
除最后一层外还有其他层不满两个子节点。
1.3 模拟链表
# 需求:模拟单向链表
# 1. 定义SingleNode, 表示: 节点类.
class SingleNode(object):
# 1.1 初始化节点的属性.
def __init__(self, item):
# 1.2 接收由用户传入的: 数值, 放到数值域中.
self.item = item
# 1.3 地址域默认为: None
self.next = None
# 2. 定义SingleLinkedList, 表示: 链表类.
class SingleLinkedList(object):
# 2.1 初始化链表的属性.
def __init__(self, head=None):
# 2.2 默认: 头节点为: None
self.head = head
# 定义各种行为
# 2.2 is_empty(self) 链表是否为空
def is_empty(self):
return self.head is None
# 2.3 length(self) 链表长度
def length(self):
# 定义变量记录链表长度
count = 0
# 定义变量表示当前节点,初始值为头节点
cur = self.head
# 判断当前节点是否为空,不为空就继续遍历
while cur is not None:
# 计数器加1
count += 1
# 更新当前节点地址域下一个节点(即后移)
cur = cur.next
# 返回链表的长度
return count
# 2.4 travel(self) 遍历整个链表
def travel(self):
# 定义变量表示当前节点,初始值为头节点
cur = self.head
# 如果当前节点不为空就一直遍历
while cur is not None:
# 打印当前节点
print(cur.item)
# 更新当前节点为下一个节点
cur = cur.next
# 2.5 add(self, item) 链表头部添加元素
def add(self,item):
# 把传入的item封装成节点
new_node = SingleNode(item)
# 设置新节点地址域为头节点
new_node.next = self.head
# 设置新节点为新的头节点
self.head = new_node
# 2.6 append(self, item) 链表尾部添加元素
def append(self, item):
# 封装传入的元素为节点
new_node = SingleNode(item)
# 判断链表是否为空
if self.is_empty():
# 为空 把元素设置为头节点(即第一个节点)
self.head = new_node
else:
# 定义变量 表示当前节点为头节点(从头开始)
cur = self.head
# 如果当前节点的地址域不为空就表示还有下一个节点 则 继续遍历
while cur.next is not None:
# 更新当前节点地址域
cur = cur.next
# 没有下一个 表示已找到最后位置 则把地址域设置成新添加的节点
cur.next = new_node
# 2.7 insert(self, pos, item) 指定位置添加元素
def insert(self,pos,item):
"""
:param pos: 要插入节点的位置
:param item: 插入的元素
:return: 无
"""
# 判断要插入的位置是否在开头
if pos <= 0:
# 往头部添加
self.add(item)
# 判断添加位置是否在结尾
elif pos >= self.length():
# 尾部添加
self.append(item)
else:
# 走到这里说明是往中间位置添加
# 定义变量 当前节点前一个节点
cur = self.head
# 定义变量表示 要插入位置前的那个节点索引
count = 0
# 只要不是要插入节点前的节点就一直遍历
while count < pos - 1:
# 更新当前节点 继续向后找
cur = cur.next
# 计数器(索引)加1
count += 1
# 找到位置 先把传入的元素封装成节点
new_node = SingleNode(item)
# 先设置新节点的地址域为当前地址域
new_node.next = cur.next
# 再设置节点的地址域为新节点
cur.next = new_node
# 2.8 remove(self, item) 删除节点
def remove(self,item):
# 定义变量表示要被删除的元素 从头节点开始的向后找
cur = self.head
# 定义变量表示要被删除的节点的前一个节点 初始值None
pre = None
# 判断当前节点不为空就一直遍历
while cur is not None:
# 判断是不是要删的元素
if cur.item == item:
# 到这是目标元素 判断cur是否为头节点 若是把地址域设置为新的头节点
if cur == self.head:
self.head = cur.next
else:
# 不是头节点(假设在中间位置) 设置它前一个节点地址域为cur的地址域
pre.next = cur.next
break
else:
# 没找到
# 若当前节点已经是要被删除的节点前一个节点了 就更新当前节点地址域向下寻找
pre = cur
cur = cur.next
# 2.9 search(self, item) 查找节点是否存在
def search(self,item):
# 定义变量表示当前节点 从头开始
cur = self.head
# 判断当前节点是否为空,不为空就一直遍历
while cur is not None:
# 判断当前元素的数值域与目标元素是否相同
if cur.item == item:
# 走这里说明找到了
return True
# 走这里是没找到 跟新地址域继续找
cur = cur.next
# 遍历完 没找到 返回False
return False
# 3. 在main方法中测试
if __name__ == '__main__':
# 3.1 创建节点类对象
node1 = SingleNode('悟*')
# 3.2 测试打印该节点的属性
print(node1.item) # 数值域, 悟*
print(node1.next) # 地址域, None
# 3.2 创建链表类对象.
linkedlist1 = SingleLinkedList() # 空链表.
# 创建链表类对象linkedlist2 传参.
linkedlist2 = SingleLinkedList(node1)
# 3.3尾部添加
linkedlist2.append('*戒')
# 3.4 测试 开头添加
linkedlist2.add('唐*')
# 3.5 测试 中间插入
linkedlist2.insert(1,'小白*')
linkedlist2.insert(2,'*沙')
# 3.6 测试 删除
linkedlist2.remove('小白*')
# 测试查找
print(linkedlist2.search('唐*'))
# 3.3判空
print(linkedlist2.is_empty())
# 3.4判断长度
print(linkedlist2.length())
# 3.5遍历整个链表
linkedlist2.travel()
1.4 模拟二叉树
# 案例: 自定义代码, 模拟二叉树结构(行为只有添加和深度优先遍历)。
# 1. 定义节点类.
class Node(object):
def __init__(self,item): # 初始化属性
self.item = item # 数值域, 存储节点内容的
self.lchild = None # 节点的 左子树(的地址), left
self.rchild = None # 节点的 右子树(的地址), right
# 2. 创建 二叉树类.
class BinaryTree(object):
# 2.1 初始化属性
def __init__(self,root = None):
self.root = root # 充当根节点
# 2.2 定义add函数(), 表示添加元素。
def add(self,item):
# 1. 判断根节点是否存在, 如果不存在, 则该(新节点)就是 根节点.
if self.root is None:
self.root = Node(item)
return # 细节: 添加后, 本次添加动作完毕, 记得终止.
# 2. 走到这里, 说明根节点存在, 我们需要找到 "缺失的节点", 就是新节点要添加的位置.
# 创建1个队列, 用于记录已存在的节点.
queue = []
# 把根节点加到队列中.
queue.append(self.root)
# 3.循环遍历队列, 直至 把新元素添加到合适的位置.
while True:
# 4. 获取队列的第1个元素(如果首次操作, 则是在获取 根节点)
node = queue.pop(0)
# 5. 判断当前节点的左子树是否存在.
if node.lchild is None:
# 5.1 如果当前节点的 左子树不存在, 就把新节点添加到这里.
node.lchild = Node(item)
return # 细节: 添加后, 本次添加动作完毕, 记得终止.
else:
# 5.2 如果当前节点的 左子树存在, 就将其添加到队列中.
queue.append(node.lchild)
# 6. 判断当前节点的右子树是否存在.
if node.rchild is None:
# 6.1 如果当前节点的 右子树不存在, 就把新节点添加到这里.
node.rchild = Node(item)
return # 细节: 添加后, 本次添加动作完毕, 记得终止.
else:
# 6.2 如果当前节点的 右子树存在, 就将其添加到队列中.
queue.append(node.rchild)
# 2.4 定义 preorder_travel(), 表示: 深度优先遍历之先序。
def preorder_travel(self, root):
# 1.判断根节点是否存在
if root is not None:
# 2.根-左-右
print(root.item,end='') # 根
# 3.递归方式获取左子树的所有元素 注意:此时是函数自己调用自己 是递归 此时参数形参root变成了root.lchild
self.preorder_travel(root.lchild)
# 4.递归方式获取右子树的所有元素
self.preorder_travel(root.rchild)
# 2.5 定义 inorder_travel(), 表示: 深度优先遍历之中序。
def inorder_travel(self, root):
# 1.判断根节点是否存在 存在在获取元素
if root is not None:
# 2.中序 左-根-右
self.inorder_travel(root.lchild)
print(root.item,end='')# 根
self.inorder_travel(root.rchild)
# 2.6 定义 postorder_travel(), 表示: 深度优先遍历之后序。
def postorder_travel(self,root):
# 1.判断根节点是否存在 存在 就获取元素
if root is not None:
# 2.后序 左 右 根
self.postorder_travel(root.lchild)
self.postorder_travel(root.rchild)
print(root.item,end='') # 根
# 3. 定义 demo01_测试节点类和二叉树类()
def demo01_测试():
# 3.1 创建节点对象.
node1 = Node('悟*')
# 3.3 创建二叉树对象.
bt = BinaryTree()
# print(f'根节点: {bt.root}') # None
bt2 = BinaryTree(node1)
# print(f'根节点: {bt2.root}')
print(f'添加前根节点的内容: {bt2.root.item}')
# 测试添加节点
bt2.add('*戒')
bt2.add('*沙')
# 每次只能拿一个节点的数据,如果想拿所有需要加上循环或者遍历加上队列(列表的思想)去拿.
print(f'添加后当前树左子树内容为{bt2.root.lchild.item}')
print(f'添加后当前树右子树内容为{bt2.root.rchild.item}')
bt2.preorder_travel(bt2.root)
print('')
bt2.inorder_travel(bt2.root)
print('')
bt2.postorder_travel(bt2.root)
if __name__ == '__main__':
demo01_测试()
二、算法
2.1 概述
编程中算法简单理解就是解决问题(需求)的思想、方式、方法。
2.2 特征
有输入、有输出、有穷性、确定性、执行性。
2.3 算法的优劣
我们评判一个算法的好坏是有标准的,一般从时间复杂度和空间复杂度两个维度来讲。其中主要使用时间复杂度通过大O标记法记录。常见的时间复杂度有O(1)、O(n)、O(nlogn)、O(n²)、O(n³)。
时间复杂度又分为最优时间复杂度(最理想状态完成)、最坏时间复杂度(最多步骤、最长时间完成)、平均时间复杂度(一定操作次数内完成的趋势走向)。没有特殊说明我们一般说的都是最坏时间复杂度,这也是标准或者说是底线。
2.4 二分查找
模拟一个经典算法—二分查找,代码如下:
# 非递归版
def binary_search(my_list,item):
# 定义两个变量表示查找的范围
star = 0 # 第一个元素的索引
end = len(my_list)-1 # 表示最后一个元素的索引
# 只要开始索引不大于最后索引就要一直遍历
while star <=end:
# 获取中间索引的位置
mid = (star+end)//2
# # 如果目标元素是中值 就直接输出
if item ==my_list[mid]:
return True
# 目标小于中值
elif item < my_list[mid]:
# 更新结束索引 为中值前一个 (即向中值前查找)
end = mid-1
else:
# 否则目标比中值大 则向中值后查找
star = mid +1
# 未找到返回Flase
return False
if __name__ == '__main__':
my_list1 = [1,5,32,43,43]
print(binary_search(my_list1, 43))
总结
回想开头讲的那个例子我们不难理解:一将无能累死三军是怎么回事儿。当然这样也直接说明有数据结构和算法加成的程序员是何等的“哇塞”。话说回来,数据结构与算法虽然是迎娶白富美 , 走上人生巅峰的必备神器,但冰冻三尺非一日之寒 , 数据结构与算法需要我们长时间的积累与不断的实践。所以今天分享的这些也只是其冰山一角。(Ps:关注,不迷路。技术分享持续更新中。。。。。。)