一、数据结构
1、数组
my_array = [ 1 , 2 , 3 , 4 , 5 ]
print ( my_array)
数组是计算机中常用的数据结构之一,它是一种线性结构,由一组具有相同类型的元素组成,这些元素在内存中是连续存储的。 数组可以通过下标进行访问和操作,下标从0开始。数组的长度是固定的,一旦创建,就无法再改变其大小。 数组可以用于存储一组有序的数据,例如整型、浮点型、字符型等。使用数组可以方便地进行数据的存取、修改、排序、查找等操作。 数组是其他数据结构的基础,例如队列、堆栈等。在算法设计中,数组是常用的数据结构之一,可以用于解决各种问题,例如排序、查找、最大子序列和等。
2、链表
class Node :
'''链表的节点
包含一个值和一个指向下一个节点的指针
'''
def __init__ ( self, val) :
self. val = val
self. next = None
class LinkedList :
'''链表
包含一个头指针,即链表的第一个节点
'''
def __init__ ( self) :
self. head = None
def add ( self, val) :
if not self. head:
self. head = Node( val)
else :
curr = self. head
while curr. next :
curr = curr. next
curr. next = Node( val)
链表是一种线性数据结构,它的每个节点包含了一个值和一个指向下一个节点的指针。 与数组相比,链表的插入和删除操作更为高效,因为它们不需要移动其他元素。但是,链表的访问操作比较耗时,因为它们需要从头开始遍历链表才能找到指定的元素。 链表有多种类型,包括单链表、双向链表和循环链表等。 链表的使用场景非常广泛,比如在实现哈希表、队列、栈等数据结构时,都可以使用链表来实现。在算法中,链表经常用来解决一些需要删除或插入元素的问题。
3、栈
class Stack :
'''栈
包含一个数组
'''
def __init__ ( self) :
self. items = [ ]
def is_empty ( self) :
return len ( self. items) == 0
def push ( self, item) :
self. items. append( item)
def pop ( self) :
return self. items. pop( )
def peek ( self) :
return self. items[ - 1 ]
def size ( self) :
return len ( self. items)
栈是一种线性数据结构,遵循后进先出(LIFO)的原则。 栈有两个基本操作:push和pop。push操作将一个元素压入栈顶,而pop操作则将栈顶元素弹出。 栈有一个重要的特性是它只能从栈顶访问元素,因此访问其他位置的元素需要先弹出栈顶的元素。 栈在计算机科学中有广泛的应用,比如函数调用栈、括号匹配、表达式求值等。
4、队列
class Queue :
'''队列
包含一个数组
'''
def __init__ ( self) :
self. items = [ ]
def enqueue ( self, item) :
self. items. append( item)
def dequeue ( self) :
if not self. is_empty( ) :
return self. items. pop( 0 )
def is_empty ( self) :
return len ( self. items) == 0
def size ( self) :
return len ( self. items)
队列是一种线性数据结构,遵循先进先出(FIFO)的原则。 队列有多种实现方式,包括数组实现、链表实现等。在使用队列时,我们需要关注队列的性质,如是否为空、是否已满,以及如何处理队列中的元素。 队列常用于异步处理、消息队列等场景,如生产者-消费者模型。
5、树
class Node :
'''二叉树节点
包含一个值、一个左节点指针、一个右节点指针
'''
def __init__ ( self, value) :
self. value = value
self. left = None
self. right = None
class BinaryTree :
'''二叉树
包含一个根节点,即二叉树的第一个节点
'''
def __init__ ( self, root) :
self. root = Node( root)
def print_tree ( self, traversal_type) :
if traversal_type == "preorder" :
return self. preorder_print( tree. root, "" )
elif traversal_type == "inorder" :
return self. inorder_print( tree. root, "" )
elif traversal_type == "postorder" :
return self. postorder_print( tree. root, "" )
else :
print ( "Traversal type " + str ( traversal_type) + " is not supported." )
return False
def preorder_print ( self, start, traversal) :
if start:
traversal += ( str ( start. value) + "-" )
traversal = self. preorder_print( start. left, traversal)
traversal = self. preorder_print( start. right, traversal)
return traversal
def inorder_print ( self, start, traversal) :
if start:
traversal = self. inorder_print( start. left, traversal)
traversal += ( str ( start. value) + "-" )
traversal = self. inorder_print( start. right, traversal)
return traversal
def postorder_print ( self, start, traversal) :
if start:
traversal = self. postorder_print( start. left, traversal)
traversal = self. postorder_print( start. right, traversal)
traversal += ( str ( start. value) + "-" )
return traversal
tree = BinaryTree( 1 )
tree. root. left = Node( 2 )
tree. root. right = Node( 3 )
tree. root. left. left = Node( 4 )
tree. root. left. right = Node( 5 )
print ( "前序遍历:" , tree. print_tree( "preorder" ) )
print ( "中序遍历:" , tree. print_tree( "inorder" ) )
print ( "后序遍历:" , tree. print_tree( "postorder" ) )
树是一种非线性的数据结构,由节点和边组成。 树的一个节点可以有多个子节点,但每个节点只有一个父节点,除了根节点没有父节点。 树的基本操作包括遍历、搜索、插入、删除等。 在计算机科学中,树被广泛应用于各种算法和数据结构,例如搜索树、堆、哈夫曼树等。
6、堆
class Heap :
'''堆,最小堆
包含一个列表
_sift_up() 和 _sift_down() 方法用于维护堆的性质
'''
def __init__ ( self) :
self. heap = [ ]
def push ( self, val) :
self. heap. append( val)
self. _sift_up( len ( self. heap) - 1 )
def pop ( self) :
if len ( self. heap) == 0 :
raise IndexError( "pop from an empty heap" )
val = self. heap[ 0 ]
self. heap[ 0 ] = self. heap[ - 1 ]
del self. heap[ - 1 ]
self. _sift_down( 0 )
return val
def _sift_up ( self, index) :
parent = ( index - 1 ) // 2
if index > 0 and self. heap[ index] < self. heap[ parent] :
self. heap[ index] , self. heap[ parent] = self. heap[ parent] , self. heap[ index]
self. _sift_up( parent)
def _sift_down ( self, index) :
left_child = 2 * index + 1
right_child = 2 * index + 2
smallest = index
if left_child < len ( self. heap) and self. heap[ left_child] < self. heap[ smallest] :
smallest = left_child
if right_child < len ( self. heap) and self. heap[ right_child] < self. heap[ smallest] :
smallest = right_child
if smallest != index:
self. heap[ index] , self. heap[ smallest] = self. heap[ smallest] , self. heap[ index]
self. _sift_down( smallest)
堆是一种完全二叉树,满足堆性质。堆分为最大堆和最小堆两种类型,最大堆中父节点的值大于或等于子节点的值,最小堆中父节点的值小于或等于子节点的值。 堆是一种特殊的二叉树,因此使用列表来表示堆的数据结构时,列表的下标可以表示堆中节点的位置。 堆的常用操作有插入元素、删除最小元素、查找最小元素等。 堆被广泛应用于各种算法中,如堆排序、最短路径算法、图像处理等。
7、图
class Graph :
'''简单图
包含一个邻接矩阵以及其容量
'''
def __init__ ( self, num_vertices) :
self. num_vertices = num_vertices
self. adj_matrix = [ [ 0 ] * num_vertices for _ in range ( num_vertices) ]
def add_edge ( self, v1, v2) :
self. adj_matrix[ v1] [ v2] = 1
self. adj_matrix[ v2] [ v1] = 1
def remove_edge ( self, v1, v2) :
self. adj_matrix[ v1] [ v2] = 0
self. adj_matrix[ v2] [ v1] = 0
图是由节点和边构成的一种数据结构,可以用来表示各种实际问题。 在图中,每个节点都有一个唯一的标识符,而边则表示节点之间的关系。另外,图还可以包含权重,用于表示不同节点之间的距离或者代价。 图可以分为有向图和无向图,有向图中的边有方向,而无向图中的边没有方向。 图的遍历和搜索是图算法的重要部分,包括深度优先搜索和广度优先搜索等算法。图还有很多其他的应用,例如在社交网络分析、路径规划、网络路由和图像处理等领域。
8、哈希表
class HashTable :
'''哈希表
包含一个二维数组和哈希表大小
'''
def __init__ ( self) :
self. size = 10
self. hash_table = [ None ] * self. size
def hash_func ( self, key) :
return hash ( key) % self. size
def add ( self, key, value) :
hash_key = self. hash_func( key)
if self. hash_table[ hash_key] is None :
self. hash_table[ hash_key] = [ ( key, value) ]
else :
for pair in self. hash_table[ hash_key] :
if pair[ 0 ] == key:
pair[ 1 ] = value
return
self. hash_table[ hash_key] . append( ( key, value) )
def get ( self, key) :
hash_key = self. hash_func( key)
if self. hash_table[ hash_key] is not None :
for pair in self. hash_table[ hash_key] :
if pair[ 0 ] == key:
return pair[ 1 ]
return None
哈希表是一种基于哈希函数实现的数据结构,通过哈希函数将数据映射到固定大小的数组中,以实现快速的数据查找和插入。 哈希表的关键操作是哈希函数的设计,它应该能够将数据均匀地映射到数组中,并且具有尽可能小的冲突。 哈希表在许多应用中都有广泛的应用,例如字典、缓存等。
二、算法
1、排序算法
排序的稳定性:对于相等元素的顺序没有影响。具体来说,在排序过程中,当遇到相等的元素时,不会改变它们的相对位置,即相等元素的先后顺序不变。
冒泡排序def bubble_sort ( arr) :
n = len ( arr)
for i in range ( n) :
for j in range ( 0 , n- i- 1 ) :
if arr[ j] > arr[ j+ 1 ] :
arr[ j] , arr[ j+ 1 ] = arr[ j+ 1 ] , arr[ j]
arr = [ 64 , 34 , 25 , 12 , 22 , 11 , 90 ]
bubble_sort( arr)
print ( "排序后的数组:" )
for i in range ( len ( arr) ) :
print ( "%d" % arr[ i] , end= " " )
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数组,每次比较相邻的两个元素,如果它们的顺序错误就交换它们。 每一趟都能把最大(或最小)的元素放在数组的末尾,使得数据像泡泡一样从底部冒出来,所以称为冒泡排序。 冒泡排序的时间复杂度为O(n^2),在数据量较小的情况下效率还是比较高的。 快速排序def quick_sort ( array) :
if len ( array) <= 1 :
return array
else :
pivot = array[ 0 ]
left = [ x for x in array[ 1 : ] if x < pivot]
right = [ x for x in array[ 1 : ] if x >= pivot]
return quick_sort( left) + [ pivot] + quick_sort( right)
快速排序是一种基于分治思想的排序算法。它的基本思想是:选定一个基准值(通常为数组中的第一个元素),然后将数组中的元素分成左右两个部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值。接着对左右两个部分分别递归地进行快速排序,最后将排好序的左右两个部分合并起来即可。 快速排序的时间复杂度为O(nlogn),不需要额外的内存空间。 在最坏情况下(即数组已经排好序或者逆序),快速排序的时间复杂度将达到O(n^2),因此需要采用随机化等优化技术来避免最坏情况的出现。 归并排序def merge_sort ( arr) :
if len ( arr) <= 1 :
return arr
mid = len ( arr) // 2
left = arr[ : mid]
right = arr[ mid: ]
left = merge_sort( left)
right = merge_sort( right)
return merge( left, right)
def merge ( left, right) :
result = [ ]
i = j = 0
while i < len ( left) and j < len ( right) :
if left[ i] <= right[ j] :
result. append( left[ i] )
i += 1
else :
result. append( right[ j] )
j += 1
result += left[ i: ]
result += right[ j: ]
return result
归并排序的基本思路是将数组切割成更小的子数组,直到每个子数组都只有一个元素,然后开始递归合并相邻的子数组以生成排序好的大数组。 归并排序的时间复杂度为 O(nlogn),空间复杂度为 O(n)。 归并排序具有稳定性,即在排序过程中相同大小的元素相对位置不会改变。 插入排序def insertion_sort ( arr) :
for i in range ( 1 , len ( arr) ) :
key = arr[ i]
j = i - 1
while j >= 0 and arr[ j] > key:
arr[ j + 1 ] = arr[ j]
j -= 1
arr[ j + 1 ] = key
return arr
插入排序的基本思路是将一个待排序的序列分成两部分,即有序部分和无序部分。遍历无序部分,将其元素插入到有序部分中,以此类推,直到整个序列有序。 插入排序的时间复杂度为
O
(
n
2
)
O(n^2)
O ( n 2 ) ,不需要额外的空间。 在面对基本有序的序列时,插入排序的性能很优秀,因为只需要进行少量的比较和移动操作。 插入排序是一种稳定排序算法,是一种原地排序算法。
2、查找算法
二分查找def binary_search ( arr, target) :
left = 0
right = len ( arr) - 1
while left <= right:
mid = ( left + right) // 2
if arr[ mid] == target:
return mid
elif arr[ mid] < target:
left = mid + 1
else :
right = mid - 1
return - 1
在使用二分查找时,需要先对数据进行排序,这样才能通过比较中间位置的值来确定目标值的位置。 二分查找的基本思想是将待查找的区间不断缩小,直到找到目标值或者确定目标值不存在。 每次将待查找区间的中间位置与目标值进行比较,如果中间位置的值等于目标值,则查找成功;如果中间位置的值小于目标值,则目标值一定在右侧区间;如果中间位置的值大于目标值,则目标值一定在左侧区间。重复以上过程,直到找到目标值或者确定目标值不存在。 二分查找是一种高效的查找算法,它的时间复杂度为 O(log n)。 二分查找适用于有序数组或者有序列表中查找目标值的场景,它的优势在于每次查找可以排除一半的数据,因此效率很高。 哈希查找def hash_search ( hash_table, value) :
key = hash ( value) % len ( hash_table)
while hash_table[ key] != None :
if hash_table[ key] == value:
return key
key = ( key + 1 ) % len ( hash_table)
return None
哈希查找,也称为散列查找,是利用哈希表进行查找的一种方法,它的平均查找时间复杂度为O(1)。 具体实现过程是将要查找的元素通过哈希函数映射到哈希表的某一个位置上,如果该位置上已经存在元素,则进行冲突处理,将该元素插入到其他位置上,直到找到空闲位置为止。 哈希查找的优点是查询速度快,适用于大规模数据的查找。 哈希查找的缺点也比较明显,即对于哈希函数的设计要求比较高,如果哈希函数设计不当,容易造成哈希冲突,影响查找效率。
3、字符串匹配算法
暴力匹配def bruteforce_match ( s, p) :
m, n = len ( s) , len ( p)
i, j = 0 , 0
while i < m and j < n:
if s[ i] == p[ j] :
i += 1
j += 1
else :
i = i - j + 1
j = 0
if j == n:
return i - j
else :
return - 1
暴力匹配算法也被称为朴素匹配算法,它是一种简单直接的字符串匹配算法。 基本思想是从主串的第一个字符开始,依次和模式串的每个字符进行比较,如果不匹配,则主串的位置后移一位,继续从下一个位置开始匹配,直到找到一个匹配的子串,或者主串匹配完毕仍然没有找到。 暴力匹配算法的时间复杂度为 O(mn),其中 m 和 n 分别为主串和模式串的长度。 在最坏情况下,需要比较主串和模式串的所有可能的子串,因此算法的时间复杂度较高。 在实际应用中,暴力匹配算法常用于短字符串匹配等规模较小的问题。 KMP算法def kmp ( s: str , p: str ) - > int :
m, n = len ( s) , len ( p)
if n == 0 :
return 0
next = [ - 1 ] * n
i, j = 0 , - 1
while i < n - 1 :
if j == - 1 or p[ i] == p[ j] :
i, j = i + 1 , j + 1
next [ i] = j
else :
j = next [ j]
i, j = 0 , 0
while i < m and j < n:
if j == - 1 or s[ i] == p[ j] :
i, j = i + 1 , j + 1
else :
j = next [ j]
if j == n:
return i - j
else :
return - 1
KMP算法是一种高效的字符串匹配算法,它的核心思想是利用已知信息尽可能减少无效的匹配操作。 具体而言,它通过一个预处理的next数组来记录模式串中的前缀和后缀的最长公共部分,然后利用这个信息避免在匹配过程中对不可能匹配的位置进行重复的尝试,即利用next数组可以让模式串在匹配失败时跳过一定长度而不是直接回到开头重新匹配。 KMP算法的核心是next数组。 KMP算法的时间复杂度为O(n+m),其中n是文本串的长度,m是模式串的长度。
4、图算法
最短路径算法
import heapq
def dijkstra ( graph, start) :
distances = { node: float ( 'inf' ) for node in graph}
distances[ start] = 0
queue = [ ( 0 , start) ]
while queue:
current_distance, current_node = heapq. heappop( queue)
if current_distance > distances[ current_node] :
continue
for neighbor, weight in graph[ current_node] . items( ) :
distance = current_distance + weight
if distance < distances[ neighbor] :
distances[ neighbor] = distance
heapq. heappush( queue, ( distance, neighbor) )
return distances
在图算法中,最短路径算法是用于寻找两个顶点之间的最短路径,即使得路径上边的权重和最小的路径。 最常用的两种最短路径算法是Dijkstra算法和Bellman-Ford算法。 Dijkstra算法的时间复杂度为O(ElogV),其中E为边数,V为节点数。这是一种比较高效的最短路径算法,适用于大多数场景。 最小生成树算法
import heapq
'''
1. 该算法基于贪心思想,它会从一个点开始,每次选择一个最短的边,将其加入生成树中,直到生成树中包含了所有的节点。
2. 该算法通过堆这个数据结构来快速找到最短的边,从而加速算法的执行。
'''
def prim ( graph, start) :
mst = [ ]
visited = set ( [ start] )
edges = [ ( cost, start, to) for to, cost in graph[ start] . items( ) ]
heapq. heapify( edges)
while edges:
cost, frm, to = heapq. heappop( edges)
if to not in visited:
mst. append( ( frm, to, cost) )
visited. add( to)
for to_next, cost in graph[ to] . items( ) :
if to_next not in visited:
heapq. heappush( edges, ( cost, to, to_next) )
return mst
最小生成树算法的目的是在一张图中找到一个生成树,使得这个生成树中所有边的权重之和最小。 基本思想是从一个点开始,每次加入一个与已有部分最近的点和这个点之间的边。这样得到的就是一个最小生成树。
5、动态规划算法
动态规划的思想:从最小的子问题开始逐步推导到原问题,即将问题拆分成更小的子问题,通过保存子问题的最优解,再逐步合并子问题的解来得到原问题的最优解。
背包问题
在该问题中,我们有一个背包和一些物品,每个物品都有一个重量和一个价值。我们的目标是用有限的背包容量装下最有价值的物品。 '''
weights: 物品的重量
values: 物品的价值
capacity: 背包容量
'''
def knapsack ( weights, values, capacity) :
n = len ( weights)
dp = [ [ 0 for j in range ( capacity + 1 ) ] for i in range ( n + 1 ) ]
for i in range ( 1 , n + 1 ) :
for j in range ( 1 , capacity + 1 ) :
if weights[ i- 1 ] > j:
dp[ i] [ j] = dp[ i- 1 ] [ j]
else :
dp[ i] [ j] = max ( dp[ i- 1 ] [ j] , dp[ i- 1 ] [ j- weights[ i- 1 ] ] + values[ i- 1 ] )
return dp[ n] [ capacity]
具体的求解方法是,对于每个物品,我们可以选择将它放入背包中或不放入背包中。如果选择放入,则背包容量需要减去物品的重量,同时背包的总价值需要加上物品的价值。如果选择不放入,则背包的容量和价值都不变。 动态规划算法中的背包问题是一个经典的问题,它可以用于解决许多实际问题,如货车装载问题、资源分配问题等。 最长公共子序列问题
def lcs ( X, Y) :
m = len ( X)
n = len ( Y)
L = [ [ None ] * ( n + 1 ) for i in range ( m + 1 ) ]
for i in range ( m + 1 ) :
for j in range ( n + 1 ) :
if i == 0 or j == 0 :
L[ i] [ j] = 0
elif X[ i- 1 ] == Y[ j- 1 ] :
L[ i] [ j] = L[ i- 1 ] [ j- 1 ] + 1
else :
L[ i] [ j] = max ( L[ i- 1 ] [ j] , L[ i] [ j- 1 ] )
return L[ m] [ n]
该算法的时间复杂度为 O(mn),其中 m 和 n 分别为两个字符串的长度。 最大子序列和问题
目标是在一个长度为 n 的数组中找到一个连续的子数组,使得该子数组中的元素之和最大。 def max_subarray_sum ( arr) :
n = len ( arr)
dp = [ 0 ] * n
dp[ 0 ] = arr[ 0 ]
for i in range ( 1 , n) :
dp[ i] = max ( arr[ i] , dp[ i- 1 ] + arr[ i] )
return max ( dp)
关键点是考虑以 arr[i]
结尾的最大子序列和应该如何计算。 如果 arr[i]
单独成为一个子序列,那么最大子序列和就是它本身,即 dp[i] = arr[i];
如果将arr[i]
加入到以arr[i-1]
结尾的最大子序列中,那么最大子序列和就是 dp[i-1]+arr[i]
。 因此,以arr[i]
结尾的最大子序列和应该是这两个数中的较大值,即 dp[i] = max(arr[i], dp[i-1]+arr[i])。