目录
排序算法
这里主要介绍插入排序、归并排序、快速排序、堆排序四种。
插入排序
插入排序相信大家都很熟悉了。。最简单的排序之一,思想就跟我们打牌时,把摸到的牌插入到自己手中正确的位置一样的。
特点:
1.稳定;
2.最坏情况下比较n*(n-1)/2次,最好情况下比较n-1次;
3.第k次排序后,前k个元素已经是从小到大排好序的
4.空间复杂度O(1)
function insertSort(arr) {
// 判断数组是否为空
if (arr == null || arr.length == 0) {
return;
}
for (var i = 1; i < arr.length; i++) {
// 从当前位置开始,只有前一位比自己大时才进行处理
if (arr[i] < arr[i - 1]) {
// 临时变量保存当前值
tmp = arr[i];
// 定义一个指针,从当前位置开始处理
j = i;
// 将比tmp大的数往后挪一个位置,给tmp空一个位置出来
while (j > 0 && arr[j-1] > tmp) {
arr[j] = arr[j-1];
j--; // 继续向前比较
}
// tmp位置填充
arr[j] = tmp;
}
console.log(arr)
}
return arr;
}
var arr = [7, 5, 8, 3, 4, 0, 9, 2, 1];
var arr1 = null;
/*
总体思路非常简单:以上数组为例:
1、首先比较7和5,5比7小,所以5要插在7前面,数组变成[5, 7, 8, 3, 4, 0, 9, 2, 1]
2、在比较5、7、8,8比7跟5都大,所以数组不变
3、比较5、7、8、3。3比8小往前插,3比7小往前插,3比5小插到5前面,这时候j已经=0了停止,
所以数组变成[3, 5, 7, 8, 4, 0, 9, 2, 1]
4、。。。后面就不在赘述了,一样的手法。
*/
insertSort(arr);
console.log(arr)
通过对插入思路的领悟可以联想到是否可以用递归来改写,刚好再复习下递归:
// 第一个参数为待排序的数组,第二个为将要往前插入的数字
function insertSortRecu(arr, i) {
// 递归终止条件
if (arr == null || arr.length <= i) {
return;
}
tmp = arr[i];
j = i;
// 跟非递归一样,从后往前,找到正确的位置插入
while (j > 0 && arr[j - 1] > tmp) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = tmp;
// 递归的插入下一个数
insertSortRecu(arr, i + 1);
}
var arr = [7, 5, 8, 3, 4, 0, 9, 2, 1];
insertSortRecu(arr, 1)
console.log(arr)
归并排序
原理
归并排序是用分治思想,将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并。
特点
def merge(arr1,arr2,arr):
"""将两个列表是arr1,arr2按顺序融合为一个列表arr,arr为原列表"""
# j和i就相当于两个指向的位置,i指arr1,j指arr2
i = j = 0
while i+j<len(arr):
# j==len(arr2)时说明arr2走完了,或者arr1没走完并且arr1中该位置是最小的
if j==len(arr2) or (i<len(arr1) and arr1[i]<arr2[j]):
arr[i+j] = arr1[i]
i += 1
else:
arr[i+j] = arr2[j]
j += 1
def merge_sort(arr):
"""归并排序"""
n = len(arr)
# 剩一个或没有直接返回,不用排序
if n < 2: return
# 拆分
mid = n // 2
arr1 = arr[0:mid]
arr2 = arr[mid:n]
# 子序列递归调用排序
merge_sort(arr1)
merge_sort(arr2)
# 合并
merge(arr1,arr2,arr)
arr = [7, 5, 8, 3, 4, 0, 9, 2, 1]
merge_sort(arr)
print(arr)
快速排序
原理:
快速排序实现的重点在于数组的拆分,通常我们将数组的第一个元素定义为比较元素,然后将数组中小于比较元素的数放到左边,将大于比较元素的放到右边,这样我们就将数组拆分成了左右两部分:小于比较元素的数组;大于比较元素的数组。我们再对这两个数组进行同样的拆分,直到拆分到不能再拆分,数组就自然而然地以升序排列了。
在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较大的记录一次就能交换到后面单元,关键字较小的记录一次就能交换到前面单元,记录每次移动的距离较大,因而总的比较和移动次数较少。
def quick_sort(arr, start, end):
"""快速排序"""
if start >= end: return # 递归的退出条件
mid = arr[start] # 设定起始的基准元素
low = start # low为序列左边在开始位置的由左向右移动的游标
high = end # high为序列右边末尾位置的由右向左移动的游标
while low < high:
# 如果low与high未重合,high(右边)指向的元素大于等于基准元素,则high向左移动
while low < high and arr[high] >= mid:
high -= 1
arr[low] = arr[high] # 走到此位置时high指向一个比基准元素小的元素,将high指向的元素放到low的位置上,此时high指向的位置空着,接下来移动low找到符合条件的元素放在此处
# 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
while low < high and arr[low] < mid:
low += 1
arr[high] = arr[low] # 此时low指向一个比基准元素大的元素,将low指向的元素放到high空着的位置上,此时low指向的位置空着,之后进行下一次循环,将high找到符合条件的元素填到此处
# 退出循环后,low与high重合,此时所指位置为基准元素的正确位置,左边的元素都比基准元素小,右边的元素都比基准元素大
arr[low] = mid # 将基准元素放到该位置,
# 对基准元素左边的子序列进行快速排序
quick_sort(arr, start, low - 1) # start :0 low -1 原基准元素靠左边一位
# 对基准元素右边的子序列进行快速排序
quick_sort(arr, low + 1, end) # low+1 : 原基准元素靠右一位 end: 最后
arr = [7, 5, 8, 3, 4, 0, 9, 2, 1]
quick_sort(arr, 0, len(arr) - 1)
print(arr)
堆排序
步骤:
1、将待排序的数组初始化为大顶堆。
2、将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。
3、由于第二部堆顶元素跟最后一个元素交换后,新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的处理流程,直到堆中仅剩下一个元素。
特点:
1、最坏、平均时间复杂度为O(nlgn)。
2、由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
3、空间复杂度O(1),
4、不稳定。
def maxHeap(heap,heapSize,i):
"""大顶堆"""
#找到节点i的左右子节点坐标
left = 2*i+1
right = 2*i+2
larger = i
if left<heapSize and heap[larger]<heap[left]:
larger = left
if right<heapSize and heap[larger]<heap[right]:
larger = right
#如果larger不等于i的话需要交换元素
if larger != i:
heap[i],heap[larger] = heap[larger],heap[i]
maxHeap(heap,heapSize,larger)
def buildMaxHeap(heap):
size = len(heap)
#找到堆里面最后一个带有子节点的节点,开始构建最大堆
for i in range((size-1)//2,-1,-1):
maxHeap(heap,size,i)
def maxHeapSort(heap):
#先构建一个最大堆
buildMaxHeap(heap)
#每次将堆顶的元素和最后一个节点交换,重新构建最大堆
for i in range(len(heap)-1,-1,-1):
heap[0],heap[i], = heap[i],heap[0]
maxHeap(heap,i,0)
return heap
arr = [7, 5, 8, 3, 4, 0, 9, 2, 1]
maxHeapSort(arr)
print(arr)
"""---------------------------------------------"""
def minHeap(heap,heapSize,i):
"""小顶堆"""
#找到节点i的左右子节点坐标
left = 2*i+1
right = 2*i+2
larger = i
if left<heapSize and heap[larger]>heap[left]:
larger = left
if right<heapSize and heap[larger]>heap[right]:
larger = right
#如果larger不等于i的话需要交换元素
if larger != i:
heap[i],heap[larger] = heap[larger],heap[i]
minHeap(heap,heapSize,larger)
def buildMinHeap(heap):
size = len(heap)
#找到堆里面最后一个带有子节点的节点,开始构建最小堆
for i in range((size-1)//2,-1,-1):
minHeap(heap,size,i)
def minHeapSort(heap):
#先构建一个小顶堆
buildMinHeap(heap)
#每次将堆顶的元素和最后一个节点交换,重新构建最大堆
for i in range(len(heap)-1,-1,-1):
heap[0],heap[i], = heap[i],heap[0]
minHeap(heap,i,0)
return heap
minHeapSort(arr)
print(arr)
递归、动态规划
动态规划基本步骤
以下步骤都是要求背诵的:
- 找出最优解的性质,并刻划其结构特征。
- 递归地定义最优值。
- 以自底向上的方式计算出最优值。
- 根据计算最优值时得到的信息,构造最优解。
动态规划特点:
- 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
- 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
贪心算法特点:
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局 部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个 基本要素,也是贪心算法与动态规划算法的主要区别。
在贪心算法中,仅在当前状态下做出最好选择,即局部最优 选择。它不依赖于将来所做的选择,也不依赖于子问题的解。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则 通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择, 每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
递归特点:
- 递归就是方法里直接或简洁调用自身。
- 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
- 解题通常显得很简洁,但运行效率较低。所以一般不提倡用递归算法设计程序。
- 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。
分治特点:
- 将一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题
- 将最后子问题可以简单的直接求解
- 将所有子问题的解合并起来就是原问题的解
Hanoi塔(汉诺塔)
汉诺塔问题算是非常经典的递归例题了,可以说搞懂了这个,也就懂了大半部分的递归~
设a,b,c是3个塔座。开始时,在塔座a上有一叠共n个圆 盘,这些圆盘自下而上,由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠 圆盘移到塔座b上,并仍按同样顺序叠置。在移动圆盘 时应遵守以下移动规则:
规则1:每次只能移动1个圆盘;
规则2:任何时刻都不允许将较大的圆盘压在较小的圆盘 之上;
规则3:在满足移动规则1和2的前提下,可将圆盘移至a,b,c中任一塔座上。
1.首先理解题目,
输入参数: A、B、C三个塔外加加盘子个数n
输出:总移动次数,(当然移动过程也可以输出)
2.开始找规律,将问题变成数学问题
1)n=1时:只有1个盘子,A–>B ,总共1次
2)n=2时:2个盘子,总共3次
第一次 1号盘 A-->C
第二次 2号盘 A-->B
第三次 1号盘 C-->B
3)n=3时:3个盘子,总共7次
第1次 1号盘 A---->B
第2次 2号盘 A---->C
第3次 1号盘 B---->C
第4次 3号盘 A---->B
第5次 1号盘 C---->A
第6次 2号盘 C---->B
第7次 1号盘 A---->B
不难发现规律,移动总次数为:2n - 1
移动规律为:
- 把n-1个盘子由A 移到 C
- 把第n个盘子由 A移到 B
- 把n-1个盘子由C 移到 B
cnt = 0
def move(n, x, y):
global cnt
cnt+=1
print(f'第{str(cnt)}次, move{str(n)}号盘:{x}--->{y}')
def hanoi(n, A, B, C):
if n == 1: move(1, A, B)
else:
hanoi(n - 1, A, C, B) # 将n-1个盘子由A经过B移动到C
move(n, A, B) # 最大盘子A移动到B
hanoi(n - 1, C, B, A) # 剩下的n-1盘子,由C经过A移动到B
hanoi(3, 'A', 'B', 'C')
print(f'共计{str(cnt)}次')
"""
第1次, move1号盘:A--->B
第2次, move2号盘:A--->C
第3次, move1号盘:B--->C
第4次, move3号盘:A--->B
第5次, move1号盘:C--->A
第6次, move2号盘:C--->B
第7次, move1号盘:A--->B
共计7次
"""
最长回文子串
给定一个字符串 s,找到 s中最长的回文子串。
示例:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
动态规划一般解题思路:填dp表、当前ij状态、过去ij状态、如何联合得到输出、边界条件
定义状态:题目让求什么,就把什么设置为状态
题目求s中最长的回文子串,那就判断所有子串是否为回文子串,选出最长的
因此:dp[i][j]表示s[i:j+1]是否为回文子串(这里+1是为了构造闭区间)
状态转移方程:对空间进行分类讨论(当前ij状态、过去ij状态 如何联合得到输出)
当前ij状态:头尾必须相等(s[i]==s[j])
过去ij状态:去掉头尾之后还是一个回文(dp[i+1][j-1] is True)
边界条件:只要是找过去ij状态的时候,就会涉及边界条件(即超出边界情况处理)当i==j时一定是回文j-1-(i+1)<=0,即j-i<=2时,只要当s[i]==s[j]时就是回文,不用判断dp[i+1][j-1]
dp[i][j] 为截取的子串
初始状态:这里已经直接判断j-i<=2的情况了,因此用不到初始状态,可以不设
输出内容:每次发现新回文都比较一下长度,记录i与长度
def longestPalindrome(s: str) -> str:
size = len(s)
if size == 1: return s
# 创建动态规划dynamic programing表
dp = [[False for _ in range(size)] for _ in range(size)]
# 初始长度为1,这样万一不存在回文,就返回第一个值(初始条件设置的时候一定要考虑输出)
max_len = 1
start = 0
for j in range(1, size):
for i in range(j):
# 边界条件:
# 只要头尾相等(s[i]==s[j])就能返回True
if j-i <= 2:
if s[i] == s[j]:
dp[i][j] = True
cur_len = j-i+1
# 状态转移方程
# 当前dp[i][j]状态:头尾相等(s[i]==s[j])
# 过去dp[i][j]状态:去掉头尾之后还是一个回文(dp[i+1][j-1] is True)
else:
if s[i] == s[j] and dp[i+1][j-1]:
dp[i][j] = True
cur_len = j-i+1
# 出现回文更新输出
if dp[i][j]:
if cur_len > max_len:
max_len = cur_len
start = i
return s[start:start+max_len]
复杂性函数的偏序
写出下列复杂性函数的偏序关系(即按照渐进阶从低到高排序):
递归式求解——主定理
4、递归式求解方法。
然后看三种渐进界:
定义看完直接上例子来说明:
例题
case1:
case2:
case3:
网络流
网络流:所有弧上流量的集合f={f(u,v)},称为该容量网络的一个网络流.
可行流:在容量网络G中满足以下条件的网络流f,称为可行流.
1.弧流量限制条件: 0<=f(u,v)<=c(u,v);
2:平衡条件:即流入一个点的流量要等于流出这个点的流量,(源点和汇点除外).
若网络流上每条弧上的流量都为0,则该网络流称为零流.
伪流:如果一个网络流只满足弧流量限制条件,不满足平衡条件,则这种网络流为伪流,或称为容量可行流.(预流推进有用)
最大流:在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流.
例题
边上的数字为该条边的容量,即在该条边上流过的量的上限值。最大流问题就是在满足容量限制条件下,使从起点s到终点t的流量达到最大。
做题前先看两个概念:
残存网络
上图为流网络,下图为残存网络,其中流网络中边上的数字分别是流量和容量,如4/16,那么4为边上的流量,16为边的容量。残存网络中可能会存在一对相反方向的边,与流网络中相同的边代表的是流网络中该边的剩余容量,在流网络中不存在的边代表的则是其在流网络中反向边的已有流量,这部分流量可以通过“回流”减少。例如,下图残存网络中,边<s,v1>的剩余容量为12,其反向边<v1.s>的值为4。在残存网络中,值为0的边不会画出,如边<v1,v2>。
增广路径
接下来的Ford-Fulkerson方法中,正是通过在残存网络中寻找一条从s到t的增广路径,并对应这条路径上的各边对流网络中的各边的流进行修改。如果路径上的一条边存在于流网络中,那么对该边的流增加,否则对其反向边的流减少。增加或减少的值是确定的,就是该增广路径上值最小的边。
第一步:
**第二步:**选择增广路径,更新流网络图和残存网络图
第三步:
第四步:
第五步:
所以,最大流为23.
多阶段决策的应用
最短路径问题:
线性规划的概念和构建
线性规划是研究在一组线性不等式或线性等式约束下使得某一线性目标函数取最大(或最小)的极值问题。
变量𝒙𝟏, 𝒙𝟐, 𝒙𝟑, … , 𝒙𝒏满足所有约束条件的一组值称为线性规划问题的一个可行解。
所有可行解构成的集合称为线性规划问题的可行区域。
使目标函数取得极值的可行解称为最优解。
在最优解处目标函数的值称为最优值。
如果一个线性规划没有可行解,则称该线性规划为不可行的,否则称它是可行的。
有些情况下,可能不存在最优解。通常有两种情况:
-
根本没有可行解,即给定的约束条件之间是相互排斥的,可行区域为空集。
-
目标函数没有极值,也就是说,在n维空间中的某个方向上,目标函数值可以无限增大或减少,而仍满足约束条件,此时目标函数无界。
回溯法和分支限界法的概念和主要特征
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。
• 算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。
• 如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;
• 否则,进入该子树,继续按深度优先策略搜索
回溯法的基本思想
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
分支限界法与回溯法的不同:
(1)求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
(2)搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。
常见的两种分支限界法
(1)队列式(FIFO)分支限界法
按照队列先进先出(FIFO)原则选取下一个节点为扩展节点。
(2)优先队列式分支限界法
按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。