书籍:github可搜到
算法概述和数据结构基础和树
算法与数据结构
算法:一系列程序指定,用于解决特定的运算和逻辑问题
算法的应用:运算(如大数运算避免变量溢出) ,搜索,排序,最优决策等
数据结构:数据组织、管理和存储的格式,目的是为了高效访问和修改数据
数据结构组成:
线性结构(数组,链表,栈、队列)
树
图
其他:跳表、位图、哈西链表
算法与数据结构relationship:
不同算法选用不同数据结构,如堆排序使用二叉堆
时间复杂度和空间复杂度
代码 的 运行时间 和 运行空间
时间复杂度
收到运行环境与输入规模影响,代码绝对执行时间无法估计,但是可以估计代码基本操作执行次数(执行次数与输入规模呈线性、对数、常量、多项式…)
时间复杂度:将相对执行时间函数简化为一个数量级,比如n,n方等
运行时间是常数量集,则用常数1表示
只保留时间函数的最高阶项
如果最高阶存在,省去最高阶项前面的系数
比较高效算法和低效算法差异(时间复杂度),在数据量随规模增长时,差异可以达到天差地别
空间复杂度
场景:运行程序时,需要存储一些中间的程序,一边后续指令更加方便进行
空间复杂度是对算法运行过程中临时占用空间大小的亮度
空间复杂度常有以下类型:常量空间、线性空间、二位空间、递归空间、
二叉树
二叉堆
排序
时间复杂度
O(n方):冒泡 选择 插入 希尔排序(性能略优于O(n方),又比不过O(nlogn),暂时放这里
O(nlogn): 快速排序 归并排序 堆排序
O(n)线性: 计数排序,桶排序,基数排序
稳定度: 稳定排序、不稳定排序
如果值相同的元素在排序后仍保持着排序前的顺序,则这样的排序是稳定排序。
冒泡排序
原理:过不断比较相邻元素并交换它们的位置(如果它们的顺序错误),逐步将最大(或最小)的元素“冒泡”到序列的末端
def bubble_sort(arr):
n = len(arr)
# 外层循环控制遍历轮数
for i in range(n):
swapped = False # 标记是否发生过交换
# 内层循环负责比较和交换操作
for j in range(0, n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j] # 交换元素
swapped = True # 标记发生了交换
# 如果在一轮内没有发生交换,则说明数组已经有序,提前结束循环
if not swapped:
break
# 示例使用
unsorted_list = [4, 2, 8, 3, 1]
bubble_sort(unsorted_list)
print(unsorted_list)
优化:添加标志变量判断是否已经完成排序,并跟中最后一次交换的位置,避免不必要的内层循环
插入排序
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
# 将arr[i]元素插入到已排序部分的正确位置
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
示例使用
unsorted_list = [4, 2, 8, 3, 1]
insertion_sort(unsorted_list)
print(unsorted_list)
快速排序
快速排序:分治法
1.选定一个基准元素pivot(通常是数组第一个元素)
2.初始化两个指针,分别指向数组的最左侧和最右侧
左边指针不断右移动,寻找第一个大于或等于基准元素的元素,同时右边指针不断向左移动,寻找赵第一个小于或等于基本元素的位置,当 left
找到一个比基准大的元素而 right
找到一个比基准小的元素时,交换两个元素的位置。重复此过程,直到左右指针相遇
3.基本元素归位:当左右指针相遇时,此时它们所在位置的元素就是最终应该位于基准位置的正确元素,交换基准元素与左右指针相遇时索引所在元素(当左右指针相遇时,它们之间的区域已经被划分成了两部分,一部分全部小于或等于基准元素,另一部分全部大于基准元素)。
4.递归处理数组
- 基准元素归位后,基准元素左侧的元素都小于或等于基准元素,右侧的元素都大于基准元素。
- 分别对基准元素左右两侧的子数组(不包含基准元素本身)递归地执行上述步骤,即对左半部分进行快速排序,然后对右半部分进行快速排序。
代码:
def quick_sort_stack(arr):
stack = [(0, len(arr) - 1)]
result = arr[:] # 创建数组副本以避免原地修改
while stack:
low, high = stack.pop()
if low < high:
pivot_index = partition(result, low, high)
# 将左右两个子区间压入栈中,等待后续处理
stack.append((low, pivot_index - 1))
stack.append((pivot_index + 1, high))
return result
def partition(arr, low, high):
pivot = arr[low]
i = low + 1
j = high
while True:
while i <= j and arr[i] <= pivot:
i += 1
while i <= j and arr[j] >= pivot:
j -= 1
if i <= j:
arr[i], arr[j] = arr[j], arr[i]
else:
break
arr[low], arr[j] = arr[j], arr[low] # 将基准元素归位
return j
# 示例使用
unsorted_list = [3, 6, 8, 10, 1, 2, 1, 15]
sorted_list = quick_sort_stack(unsorted_list)
sorted_list = quick_sort(unsorted_list)
print(sorted_list)
递归实现与栈实现的快速排序算法的主要区别在于处理递归调用的方式:
递归实现:
优点:递归实现的快速排序代码结构清晰,易于理解,递归调用直观体现了分治策略。
缺点:在递归过程中,系统栈会不断地累积递归调用的信息,如果递归深度过大(例如在处理大规模近乎有序数据时),可能会导致栈溢出问题
堆排序
本质:
二叉堆特性:最大堆的堆顶是整个堆中的最大元素,最小堆的堆顶是整个堆的最小元素,如果“删除”一个最大堆的堆顶(不是真的删除,而是与末尾的节点交换位置),经过自我调整,第二大的元素会被交换上堆顶,成为最大堆的新的堆顶。不断”删除“到此为止,原本的最大二叉堆已经变成了一个从小到大的有序集合。
堆排序的原理/堆排序算法的步骤。
-
把无序数组构建成二叉堆。
需要从小到大排序,则构建成最大堆;
需要从大到小排序,则构建成最小堆。
-
循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
def heapify(arr, n, i):
'''
heapify 函数用于维护堆的性质,它接收一个数组、数组长度和当前处理元素的索引,将当前元素调整到合适的位置,以保证其子树仍满足堆的性质。
:param arr:
:param n:
:param i:
:return:
'''
print(arr,n,i)
largest = i # 初始化 largest 为当前节点的索引 i,认为当前节点是子树中最大的元素。
# 计算左孩子的索引 l(2 * i + 1)和右孩子的索引 r(2 * i + 2)
l = 2 * i + 1 # left = 2*i + 1
r = 2 * i + 2 # right = 2*i + 2
# See if left child of root exists and is
# greater than root 检查左孩子是否存在(即 l < n 是否成立),如果存在且左孩子的值大于当前节点值,则更新 largest 为左孩子的索引。
if l < n and arr[i] < arr[l]:
largest = l
# See if right child of root exists and is
# greater than root 继续检查右孩子是否存在且其值大于当前 largest 所指向节点的值,如果满足条件,更新 largest 为右孩子的索引。
if r < n and arr[largest] < arr[r]:
largest = r
# Change root, if needed
if largest != i:#检查 largest 是否改变了,如果改变了,说明找到了子树中的最大元素(对于大顶堆),需要交换该最大元素与当前节点的位置
arr[i], arr[largest] = arr[largest], arr[i] # swap
# Heapify the root.
heapify(arr, n, largest) #调用自身 heapify(arr, n, largest) 来递归地对刚才成为新根节点的孩子进行堆调整,确保整个子树依然满足堆的性质。
def heapSort(arr):
'''
heapSort 函数首先构建一个大顶堆,然后将堆顶元素(最大值)与堆尾元素交换,并缩小堆的范围,接着对新的堆顶元素执行下沉操作,直至整个序列有序。
:param arr:
:return:
'''
n = len(arr)
# Build a maxheap.
for i in range(n, -1, -1):
heapify(arr, n, i)
# One by one extract elements
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # swap
heapify(arr, i, 0)
arr = [12, 11, 13, 5, 6, 7]
heapSort(arr)
n = len(arr)
print("Sorted array is", arr)
计数排序
计数排序是一种非基于比较的排序算法,它利用输入数组中每个元素的出现频次(即计数)来直接确定其在输出数组中的位置。这种排序算法特别适用于整数排序,尤其是当整数范围不是很大且数值分散时,能够以线性时间复杂度完成排序。
数排序的结构主要包括两个核心部分:
-
计数数组(Count Array):这是一个临时数组,大小与输入数组中最大值的范围相同,用于存储每个唯一元素的出现次数。例如,如果输入数组的最大值是 K,则计数数组的大小至少为 K+1。
-
输出数组(Output Array):用于存放排序后结果的数组。
def counting_sort(arr):
# 获取数组中最大值及其索引
max_val = max(arr)
min_val = min(arr)
count_arr = [0] * (max_val - min_val + 1) # 初始化计数数组
# 统计每个元素的出现次数
for num in arr:
count_arr[num - min_val] += 1
# 重建排序后的数组
sorted_arr = []
index = 0
for i, count in enumerate(count_arr):
while count > 0:
sorted_arr.append(min_val + i)
count -= 1
return sorted_arr if isinstance(arr, list) else np.array(sorted_arr)
# 示例使用
import numpy as np
unsorted_list = [4, 2, 2, 8, 3, 3, 1]
sorted_list = counting_sort(unsorted_list)
print(sorted_list)