目录
逻辑结构:指数据元素之间的逻辑关系。即从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。
逻辑结构可细分为四类:
- 集合结构: 仅同属一个集合
- 线性结构: 一对一(1:1) ——线性
- 树结构: 一对多(1:n) ——非线性
- 图结构: 多对多 (m:n) ——非线性
线性结构
线性结构:若结构是非空有限集,则有且仅有一个开始结点和一个终端结点,并且所有结点都最多只有一个直接前趋和一个直接后继。 可表示为:(a1 , a2 , ……, an) 。线性结构反映结点间的逻辑关系是1:1的。线性结构包括:线性表、堆栈、队列、字符串、数组等。
线性表比较通用;堆栈用于函数调用、递归和简化设计等;队列用于离散事件模拟、OS作业调度和简化设计等。
线性表
注意:在C语言中数组的下标是从0开始,即: V[n]的有效范围是从 V[0]~V[n-1]
线性表的顺序表示又称为顺序存储结构或顺序映像。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。即逻辑上相邻的元素,物理上也相邻。
顺序存储方法:用一组地址连续的存储单元依次存储线性表的元素。
线性表的运算:
修改:通过数组的下标便可访问某个特定元素并修改之。 V[i]=x; ——顺序表修改操作的时间效率是 O(1)
插入:在线性表的第i个位置前插入一个元素。实现步骤: 将第n至第i 位的元素向后移动一个位置; 将要插入的元素写到第i个位置; 表长加1。注意:事先应判断: 插入位置i 是否合法,表是否已满?,应当符合条件: 1≤i≤n+1 或 i=[1, n+1]。若插入在尾结点之后,则根本无需移动(特别快); 若插入在首结点之前,则表中元素全部要后移(特别慢); 应当考虑在各种位置插入(共n+1种可能)的平均移动次数才合理。故插入时的平均移动次数为:n(n+1)/2÷(n+1)=n/2≈O(n)
删除:删除线性表的第i个位置上的元素。实现步骤: 将第i+1 至第n 位的元素向前移动一个位置; 表长减1。注意:事先需要判断,删除位置i 是否合法? 应当符合条件:1≤i≤n 或 i=[1, n]。T(n)=(n-1)/2 ≈O(n)
链表
链式存储结构:其结点在存储器中的位置是随意的,即逻辑上相邻的数据元素在物理上不一定相邻。
让每个存储结点都包含两部分:数据域和指针域。
相关术语:
1)结点:数据元素的存储映像。由数据域和指针域两部分组成;
2)链表: n个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
3)单链表、双链表、多链表、循环链表:
- 结点只有一个指针域的链表,称为单链表或线性链表;
- 有两个指针域的链表,称为双链表(但未必是双向链表);
- 有多个指针域的链表,称为多链表;
- 首尾相接的链表称为循环链表。
单链表的运算:
修改:缺点:想寻找单链表中第i个元素,只能从头指针开始逐一查询(顺藤摸瓜),无法随机存取 。T(n)=O(n)。
插入:因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
删除:因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
但是,如果要在单链表中进行前插或删除操作,因为要从头查找前驱结点,所耗时间复杂度将是 O(n)。
链表中每个结点都要增加一个指针空间,相当于总共增加了n 个整型变量,空间复杂度为 O(n)。
栈
- 限定只能在表的一端进行插入和删除运算的线性表。
- 与线性表相同,仍为一对一( 1:1)关系。
- 用顺序栈或链栈存储均可,但以顺序栈更常见。
- 只能在栈顶运算,且访问结点时依照后进先出(LIFO)或先进后出(FILO)的原则。
堆栈是一种特殊的线性表,它只能在表的一端(即栈顶)进行插入和删除运算。
递归:一个直接或间接调用自身的函数被称为是递归(recursion)的。递归调用需要利用堆栈。
队列
- 只队列 (Queue)是仅在表尾进行插入操作,在表头进行删除操作的线性表。它是一种先进先出(FIFO)的线性表。
- 与线性表相同,仍为一对一关系。
- 只能在队首和队尾运算,且访问结点时依照先进先出(FIFO)的原则
串
串即字符串,是由零个或多个字符组成的有限序列,是数据元素为单个字符的特殊线性表。
数组
数组: 由一组名字相同、下标不同的变量构成。
int a[3];
int a[3][3];
非线性结构
树和二叉树
树:非线性结构,一个直接前驱,但可能有多个直接后继。1:n
二叉树:
- 二叉树的结构最简单,规律性最强;
- 可以证明,所有树都能转为唯一对应的二叉树,不失一般性。
- 每个结点最多只有两棵子树(不存在度大于2的结点)
- 左子树和右子树次序不能颠倒
二叉树的存储:
- 顺序存储结构:按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。
- 链式存储结构:一般从根结点开始存储。 (相应地,访问树中结点时也只能从根开始)。
图
多对多
查找运算
静态查找
顺序查找(线性查找)
用逐一比较的办法顺序查找关键字,这显然是最直接的办法。顺序查找有三种情形可能发生:最好的情况,第一项就是要查找的数据对象,只有一次比较,最差的情况,需要 n 次比较,全部比较完之后找不到数据。平均情况下,比较次数为 n/2 次。算法的时间复杂度是 O(n) 。
顺序查找某一值X是否存在列表中:
#在列表中查找 x 是否存在
def sequest(alist, item):
pos=0 #初始查找位置
found=False #未找到数据对象
while pos<len(alist) and not found: # 列表未结束并且还未找到则一直循环
if alist[pos] == item: # 找到匹配对象,返回TRUE
found=True
else: #否则查找位置 + 1
pos = pos+1
return found
def main():
testlist = [1, 3, 5, 6, 7, 8, 9, 11, 23, 44]
print(sequest(testlist, 11))
if __name__=='__main__':
main()
顺序查找最大、最小值:
# 在列表中顺序查找最大值和最小值
def Max(alist):
pos = 0 #初始位置
imax=alist[0] #假设第一个元素是最大值
while pos < len(alist): #在列表中循环
if alist[pos] > imax: #当前列表的值大于最大值 ,则为最大值
imax=alist[pos]
pos = pos+1 #查找位置 +1
return imax
def Min(alist):
pos = 0 # 初始位置
imin = alist[0] #假设第一个元素是最小值
for item in alist: #对于列表中的每一个值
if item < imin: #当前的值小于最小的值 则为最小值
imin = item
return imin
def main():
testlist=[2,3,4,5,6,8,34,23,55,234]
print('最大值是:',Max(testlist))
print('最小值是:',Min(testlist))
if __name__=='__main__':
main()
折半查找(二分或对分查找)
先给数据排序(例如按升序排好),形成有序表,然后再将key与正中元素相比,若key小,则缩小至右半部内查找;再取其中值比较,每次缩小1/2的范围,直到查找成功或失败为止。
时间复杂度:
举例(针对已升序排好的数据):
待查找数据集合 - value,要查找的数值 - key,限定查找范围的左侧元素下标值 - left,限定查找范围的右侧元素下标值 - right
“/”,这是传统的除法,3/2=1.5
“//”,在python中,这个叫“地板除”,3//2=1
def binary(value,key,left,right):
if left > right:
# 查找失败
return -1
middle = (left+right)//2
if value[middle] < key:
left = middle + 1
return binary(value,key,left,right)
elif value[middle] > key:
right = middle - 1
return binary(value,key,left,right)
else:
return middle
values = [3,9,10,12,25,34,45,56,67,76,84,99]
result = binary(values,25,0,len(values)-1)
if result == -1:
print('查找失败')
else:
print('查找成功,对应下标为',result)
结果:查找成功,对应下标为 4
分块查找(索引顺序查找)
先让数据分块有序,即分成若干子表,要求每个子表中的数据元素值都比后一块中的数值小(但子表内部未必有序)。 然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址(即头指针)。
特点:块间有序,块内无序。
步骤:①先选取各块中的最大关键字构成一个索引表;②对索引表使用折半查找法(因为索引表是有序表);③确定了待查关键字所在的子表后,在子表内采用顺序查找法(因为各子表内部是无序表);
# 二分查找
def binary_search(list, key):
length = len(list)
first = 0
last = length - 1
print("length:%s list:%s"%(length,list))
while first <= last:
mid = (last + first) // 2
if list[mid] > key:
last = mid - 1
elif list[mid] < key:
first = mid + 1
else:
return mid
return False
# 分块查找,count是块数,block_length是块的大小
def block_search(list, block_length, key):
length = len(list)
count = length//block_length
#count取整,若相除不是整数,则要加1
if block_length * count != length:
count += 1
print("块数:", count) # 几个块
for block_i in range(count):
block_list = []
for i in range(block_length):
if block_i*block_length + i >= length:
break
block_list.append(list[block_i*block_length + i])
result = binary_search(block_list, key)
if result != False:
return block_i*block_length + result
return False
if __name__ == '__main__':
list = [1, 5, 7, 8, 22, 54, 99, 123, 200, 222, 444]
result = block_search(list, 4, 444) # 第二个参数是块的长度,最后一个参数是要查找的元素
print("要查找的元素所在的索引是:", result)
结果:块数: 3
length:4 list:[1, 5, 7, 8]
length:4 list:[22, 54, 99, 123]
length:3 list:[200, 222, 444]
要查找的元素所在的索引是: 10
动态查找
特点:表结构在查找过程中动态生成。
要求:对于给定值key, 若表中存在其关键字等于key的记录,则查找成功返回;否则插入关键字等于key 的记录。
二叉排序树(BST)
二叉查找树(BinarySearch Tree)或者是一棵空树,或者是具有下列性质的二叉树
- 1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 3)任意节点的左、右子树也分别为二叉查找树。
将线性表构造成二叉排序树的优点:
- ① 查找过程与顺序结构有序表中的折半查找相似,查找效率高;
- ② 中序遍历此二叉树,将会得到一个关键字的有序序列(即实现了排序运算);
- ③ 如果查找不成功,能够方便地将被查元素插入到二叉树的叶子结点上,而且插入或删除时只需修改指针而不需移动元素。
时间复杂度:
- 最坏情况(单支树) :插入的n个元素从一开始就有序,此时树深为n ; ASL= (n+1)/2 ; 与顺序查找情况相同。
- 最好情况(形态均衡) :与折半查找中的判定树相同,此时树深为:log 2n +1 ; ASL=log 2(n+1) –1 ;与折半查找情况相同。
二叉树的遍历:它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
二叉树遍历的时间效率和空间效率 时间效率:O(n),每个结点只访问一次; 空间效率:O(n),栈占用的最大辅助空间。
深度遍历:用递归来实现
- 先序遍历,即先根再左再右
- 中序遍历,即先左再根再右
- 后序遍历,即先左再右再根
class Node:
"""节点类"""
def __init__(self, elem, lchild=None, rchild=None):
self.elem = elem
self.lchild = lchild
self.rchild = rchild
class Tree:
"""树类"""
def __init__(self, root=None):
self._root = root
def add(self, item):
node = Node(item)
if not self._root:
self._root = node
return
queue = [self._root]
while queue:
cur = queue.pop(0)
if not cur.lchild:
cur.lchild = node
return
elif not cur.rchild:
cur.rchild = node
return
else:
queue.append(cur.rchild)
queue.append(cur.lchild)
def preorder(self, root):
"""
先序遍历-递归实现
"""
if not root:
raise ValueError("ROOT ERROR")
print(root.elem)
self.preorder(root.lchild)
self.preorder(root.rchild)
def inorder(self, root):
"""
中序遍历-递归实现
"""
if not root:
raise ValueError("ROOT ERROR")
self.inorder(root.lchild)
print(root.elem)
self.inorder(root.rchild)
def postorder(self, root):
"""
后序遍历-递归实现
"""
if not root:
raise ValueError("ROOT ERROR")
self.postorder(root.lchild)
self.postorder(root.rchild)
print(root.elem)
广度遍历:广度遍历是从树的根层级开始一层一层的遍历,遍历完上一层再遍历下一层,用队列来实现
class Node:
"""节点类"""
def __init__(self, elem, lchild=None, rchild=None):
self.elem = elem
self.lchild = lchild
self.rchild = rchild
class Tree:
"""树类"""
def __init__(self, root=None):
self._root = root
def breadth_travel(self, root):
"""
广度优先-队列实现
"""
if not root:
raise ValueError("ROOT ERROR")
queue = [root]
while queue:
node = queue.pop(0)
print(node.elem)
if node.lchild:
queue.append(node.lchild)
elif node.rchild:
queue.append(node.rchild)
哈希查找
哈希查找算法,依赖哈希表这种数据结构,它是以键值对的形式存储数据,对于哈希查找算法来说,其时间复杂度为O(1),Redis数据表中也有哈希存储,储存形式是键值对。哈希查找的产生有这样一种背景——有些数据本身是无法排序的(如图像),有些数据是很难比较的(如图像)。如果数据本身是无法排序的,就不能对它们进行比较查找。如果数据是很难比较的,即使采用折半查找,要比较的次数也是非常多的。因此,哈希查找并不查找数据本身,而是先将数据映射为一个整数(它的哈希值),并将哈希值相同的数据存放在同一个位置一即以哈希值为索引构造一个数组。在哈希查找的过程中,只需先将要查找的数据映射为它的哈希值,然后查找具有这个哈希值的数据,这就大大减少了查找次数。
哈希表:即散列存储结构。
散列法存储的基本思想:建立关键码字与其存储位置的对应关系,或者说,由关键码的值决定数据的存储地址。
优点:查找速度极快(O(1)),查找效率与元素个数n无关!
哈希算法:选取某个函数,依该函数按关键字计算元素的存储位置并按此存放;查找时也由同一个函数对给定值k计算地址,将k与地址中内容进行比较,确定查找是否成功。通常关键码的集合比哈希地址集合大得多,因而经过哈希函数变换后,可能将不同的关键码映射到同一个哈希地址上,这种现象称为冲突。在哈希查找方法中,冲突是不可能避免的,只能尽可能减少。
board = [None for i in range(n)]得到的是一个n*n的列表,相当于
board
=
[]
for
i
in
range
(n):
list_1
=
[]
for
j
in
range
(n):
list_1.append(
None
)
board.append(list_1)
python实现哈希数据结构及其哈希查找算法:
class HashTable:
def __init__(self, size):
self.elem = [None for i in range(size)] # 使用list数据结构作为哈希表元素保存方法
self.count = size # 最大表长
def hash(self, key):
return key % self.count # 散列函数采用除留余数法
def insert_hash(self, key):
"""插入关键字到哈希表内"""
address = self.hash(key) # 求散列地址
while self.elem[address]: # 当前位置已经有数据了,发生冲突。
address = (address + 1) % self.count # 线性探测下一地址是否可用
self.elem[address] = key # 没有冲突则直接保存。
def search_hash(self, key):
"""查找关键字,返回布尔值"""
star = address = self.hash(key)
while self.elem[address] != key:
address = (address + 1) % self.count
if not self.elem[address] or address == star: # 说明没找到或者循环到了开始的位置
return False
return True
排序运算
冒泡排序
基本思路:每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。比较第一个和第二个元素,看是否交换,再比较第二个第三个元素,看是否交换...一遍遍排序直到结束
优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有 交换发生,还可以提前结束排序。
前提:顺序存储结构
最优时间复杂度:O(n)
最坏时间复杂度:O(n²)
稳定性:稳定
def bubble_sort(nums):
for i in range(len(nums) - 1):
for j in range(len(nums) - i - 1):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
return nums
if __name__ == '__main__':
l = [33, 11, 26, 78, 3, 9, 40]
print(l)
bubble_sort(l)
print(l)
快速排序
基本思想:从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表; 然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。
优点:效率高,数据移动比较少,数据量越大,优势越明显
前提:顺序存储结构
快速排序的平均排序效率为O(nlog2n); 但最坏情况下(例如天然有序)仍为O(n^2)
#first理解为第一个位置的索引,last是最后位置索引
def quick_sort(alist, first, last):
# 递归终止条件
if first >= last:
return
# 设置第一个元素为中间值
mid_value = alist[first]
# low指向
low = first
# high
high = last
# 只要low小于high就一直走
while low < high:
# high大于中间值,则进入循环
while low < high and alist[high] >= mid_value:
# high往左走
high -= 1
# 出循环后,说明high小于中间值,low指向该值
alist[low] = alist[high]
# high走完了,让low走
# low小于中间值,则进入循环
while low < high and alist[low] < mid_value:
# low向右走
low += 1
# 出循环后,说明low大于中间值,high指向该值
alist[high] = alist[low]
# 退出整个循环后,low和high相等
# 将中间值放到中间位置
alist[low] = mid_value
# 递归
# 先对左侧快排
quick_sort(alist, first, low - 1)
# 对右侧快排
quick_sort(alist, low + 1, last)
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
quick_sort(li, 0, len(li) - 1)
print(li)
插入排序
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。简言之,边插入边排序,保证子序列中随时都是排好序的。
时间效率: 因为在最坏情况下,所有元素的比较次数总和为(0+1+…+n-1)→O(n^2)。其他情况下也要考虑移动元素的次数。 故时间复杂度为O(n^2)
空间效率:仅占用1个缓冲单元——O(1)
算法的稳定性:——稳定
python实现插入排序并不难,从第二个位置开始遍历,与它前面的元素相比较,如果比前面元素小就交换位置,实现如下:
def insert_sort(items):
for i in range(1, len(items)):
# 从第i个元素开始向前比较,如果小于前一个元素,交换位置
for j in range(i, 0, -1):
if items[j] < items[j-1]:
items[j], items[j-1] = items[j-1], items[j]
if __name__ == '__main__':
l = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(l)
insert_sort(l)
print(l)
希尔排序
希尔排序是插入排序的一种,又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的核心是对步长的理解,步长是进行相对比较的两个元素之间的距离,随着步长的减小,相对元素的大小会逐步区分出来并向两端聚拢,当步长为1的时候,就完成最后一次比较,那么序列顺序就出来了。
def shellSort(nums):
# 设定步长
n = len(nums)
#" / "就表示 浮点数除法,返回浮点结果;" // "表示整数除法
step = n // 2
while step > 0:
for i in range(step, n):
# 类似插入排序, 当前值与指定步长之前的值比较, 符合条件则交换位置
while i >= step and nums[i-step] > nums[i]:
nums[i], nums[i-step] = nums[i-step], nums[i]
i -= step
step = step//2
return nums
if __name__ == '__main__':
l = [9, 3, 5, 8, 3, 2, 7, 1]
print(l)
shellSort(l)
print(l)
选择排序
把序列中的最小值或者最大值找出来放在起始位置,然后再从剩下的序列中找出极值放到起始位置之后,以此类推最后就完成排序。
完成这个过程大致思想:首先需要一个记录器,记录排序排到第几个位置了,然后在剩余的序列中找到极值下标,最后将记录器位置和极值位置元素交换,完成本次选择排序。
最优时间复杂度:O(n²)
最坏时间复杂度:O(n²)
稳定性:不稳定
优点:移动次数少
缺点:比较次数多
def select_sort(items):
n = len(items)
for i in range(n-1):
item_max = i
# 外层控制比较几轮
for j in range(i+1, n):
if items[j] > items[item_max]:
items[j], items[item_max] = items[item_max], items[j]
#更新索引
if item_max != i:
items[i], items[item_max] = items[item_max], items[i]
if __name__ == '__main__':
l = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(l)
select_sort(l)
print(l)
归并排序
归并排序的基本思想是:将两个(或以上)的有序表组成新的有序表。
更实际的意义:可以把一个长度为n 的无序序列看成是 n 个长度为 1 的有序子序列 ,首先做两两归并,得到 n / 2 个长度为 2 的有序子序列 ;再做两两归并,…,如此重复,直到最后得到一个长度为 n 的有序序列。
时间效率: O(nlog2n)
空间效率: O(n)
优点:稳定,数据量越大越优秀
缺点:需要额外空间
def merge_sort(alist):
n = len(alist)
# 递归结束条件
if n <= 1:
return alist
# 中间位置
mid = n // 2
# 递归拆分左侧
left_li = merge_sort(alist[:mid])
# 递归拆分右侧
right_li = merge_sort(alist[mid:])
# 需要2个游标,分别指向左列表和右列表第一个元素
left_point, right_point = 0, 0
# 定义最终返回的结果集
result = []
# 循环合并数据
while left_point < len(left_li) and right_point < len(right_li):
# 谁小谁放前面
if left_li[left_point] <= right_li[right_point]:
# 放进结果集
result.append(left_li[left_point])
# 游标移动
left_point += 1
else:
result.append(right_li[right_point])
right_point += 1
# 退出循环时,形成左右两个序列
result += left_li[left_point:]
result += right_li[right_point:]
return result
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
sort_li = merge_sort(li)
print(sort_li)