算法思想
算法思想
一、前言
从考研结束就一直学习算法和数据结构。经过几个月的学习是时候写一些总结了。程序是由数据结构和算法组成的,数据结构的介绍在另一篇文章上,这里不再过多的赘述。这篇文章主要进行算法思想的总结。算法思想主要包括:分而治之、贪婪算法、动态规划、回溯、哈希表等等。为了便于说明这些思想,这篇文章举例相关的题目进行解说。
二、排序
排序有多种方法,常见的有冒泡排序、选择排序、插入排序、快速排序、堆排序等。
(1)冒泡排序
冒泡排序是最基本的一种排序方法,思路:比较相邻的两个元素,做比较大小,将大的元素向后移动(如果是升序排序,降序排序反之。),循环一次,此时已经将最大的元素排列到最后。重复上述循环直到排序完成。
冒泡排序的实现
#python实现 def bubSort(self,nums:list)->list: count = len(nums) #[1,2,3,4,5] for i in range(count): for j in range(count-i-1): if nums[j]>nums[j+1]: nums[j+1],nums[j] = nums[j],nums[j+1] return nums
//使用java实现 public void pubSotd(int [] nums) { int count = nums.length; int temp = 0; for(int i=0;i<count;i++) { for (int j=0;j<count-i-1;j++) { if(nums[j]>nums[j+1]) { temp = nums[j]; nums[j] = nums[j+1]; nums[j+1] = temp; } } } }
(2)快速排序
由于冒泡排序的思想和插入排序以及选择排序的思想都差不多,在这只实现冒泡排序。采用分而治之的思想实现的快速排序。
算法思路:随机抽取一个元素(假设为n),遍历该数组,将大于n的元素放在n后面(如果是升序排列采用该方法,降序排列反之),小于n的元素放在后面。在递归执行分好组的其他没有分组的元素。直到实现排序。
#python实现 class Solution: #现实快速排序 #这个函数是切割函数 def cut(self,nums:list): #抽取第一个 priot = nums[0] i = 0 j = len(nums) - 1 while i<=j: if nums[i] > priot: nums[i],nums[j] = nums[j],nums[i] j-=1 else: i+=1 nums[0],nums[i-1] = nums[i-1],nums[0] return i-1 def quickSort(self,nums:list): if len(nums) == 1: return nums elif len(nums)==0: return [] else: t = self.cut(nums) #print(nums) l = self.quickSort(nums[:t+1]) r = self.quickSort(nums[t+1:]) l.extend(r) return l
(3)归并排序
归并排序的思想主要是把大问题划分为小问题,然后解决所有的小问题就可以解决当前的大问题。归并排序利用递归,多次递归将大的排序问题分成多个子问题,同样利用调用归并排序算法。
可以如下图所示:
#使用python实现归并排序算法 class Solution: #归并排序 def m(self,nums1:list,nums2:list)->list: i = 0 j = 0 n = [] while i<len(nums1) and j<len(nums2): if nums1[i]<nums2[j]: n.append(nums1[i]) i+=1 else: n.append(nums2[j]) j+=1 while i<len(nums1): n.append(nums1[i]) i+=1 while j<len(nums2): n.append(nums2[j]) j+=1 return n def mergeSort(self,nums:list)->list: if len(nums)==1: return nums if len(nums)==0: return [] long = len(nums) mid = long//2 pre = self.mergeSort(nums[:mid]) last = self.mergeSort(nums[mid:]) return self.m(pre,last)
(4)堆排序
堆是一种数据结构。它类似二叉树结构,堆的特点是父节点要大于子节点的值,如果是倒堆父节点要小于子节点的值。由于堆类似于二叉树,因此,堆它符合一些二叉树的性质。假设堆有一个节点i,该i的值为二叉树层次排序的值(所谓二叉树层次排序的值为:第一层第一个为1,第二层第一个为2,第二层第二个为3,第三层第一个为4,第三层第二个为5,依次类推)。则该i节点的父节点值就为((i+1)/2)-1,该i节点的左孩子为2(i+1)-1,该i节点的右孩子为2(i+1)。根据这个性质我们就可以用数组去构造一个堆。
利用下标作为i值,就可以确定他们的父子关系。
在进行堆排序的时候应该需要将数组构造成堆。构成堆的思路为:遍历整个数组判断子节点和父节点的大小,如果不符合大堆的性质就交换。
#python的代码实现 class Heap: def parent(self, i: int) -> int: return int((i + 1) / 2) - 1 def left(self, i: int) -> int: return 2 * (i + 1) - 1 def right(self, i: int) -> int: return 2 * (i + 1) def BuildHeap(self, nums: list) -> list: if len(nums) <= 1: return nums k = 1 while k < len(nums): i = k while self.parent(i) >= 0: if nums[self.parent(i)] < nums[i]: nums[self.parent(i)], nums[i] = nums[i], nums[self.parent(i)] i = self.parent(i) else: break k += 1
此时要是实现堆排序的第一步构造堆已经实现了,在构造这个堆的时候我们已经发现,该堆的堆顶就是数组中最大值。要实现堆排序,接下来就是取出堆顶,重新构造堆,重复步骤就可以实现排序。由于我们构造的是一个大堆,我们只要把堆顶的元素于数组的最后一个元素交换即可。对于这个操作我们面临着一个操作是交换之后如何重构n-1个元素的堆。
堆为
首先交换堆顶于最后一个元素的位置,并且要实现最后一个元素于堆的联系
#python的实现 class Heap: def parent(self, i: int) -> int: return int((i + 1) / 2) - 1 def left(self, i: int) -> int: return 2 * (i + 1) - 1 def right(self, i: int) -> int: return 2 * (i + 1) def BuildHeap(self, nums: list) -> list: if len(nums) <= 1: return nums k = 1 while k < len(nums): i = k while self.parent(i) >= 0: if nums[self.parent(i)] < nums[i]: nums[self.parent(i)], nums[i] = nums[i], nums[self.parent(i)] i = self.parent(i) else: break k += 1 return nums def maxifyHeap(self, heap: list, size: int): parent = 0 while self.left(parent) < size or self.right(parent) < size: maxVal = heap[parent] child = parent if self.left(parent) < size and heap[self.left(parent)] > maxVal: maxVal = heap[self.left(parent)] child = self.left(parent) if self.right(parent) < size and heap[self.right(parent)] > maxVal: maxVal = heap[self.right(parent)] child = self.right(parent) if child == parent: return heap[parent], heap[child] = heap[child], heap[parent] parent = child def heapSort(self, nums: list) -> list: nums = self.BuildHeap(nums) heapSize = len(nums) while heapSize > 1: nums[0], nums[heapSize - 1] = nums[heapSize - 1], nums[0] heapSize -= 1 self.maxifyHeap(nums, heapSize) return nums
三、分而治之思想
现实生活中有很多的问题看似很难,但是却可以分解成多个不难解决的小问题,当解决这些小问题的时候,这些小问题组成的当前较难的大问题也就解决了,这就是分而治之的基本思想。分而治之一般都与递归有关系,当前要解决的问题分解各个小问题一般他们都具有相同的解决方案,这样通过递归调用就可以进行解决。为了说明这个思想我们可以举一个具体的实例。
假如有个大小为n的数组,我们要从这个数组中找到一个与原数组顺序相同的子数组,使这个子数组的和为最大。讨论这个问题我们首先分析最大和的数组所在的位置。可以用暴力的解法遍历包含每个元素的子数组,但是效率太低,我们利用分而治之的思想去思考,先把当前的数组进行分割,分割成两个大小相当的数组。这时解决问题变成了解决了这两个分割数组中的最大子数组。
在解决这个问题时出现了三种情况:
此时我们只需要思考第三种情况,其他两种情况和我们的大问题是属于一样性质的问题,我们已经把它规模通过分而治之的思想转化为了较小规模的小问题。
现在只需要把左侧数组从尾巴处索引进行向左累加确定左侧最大值,同理,右侧相同的处理,只不过要从开头处向右累加确定右侧最大值。比较中间的值和左右两侧的值进行判断取留那个子数组。
根据上述分析我们可以画出程序流程图:
我们着手实现我们的思想:
#python代码 class Solution: # 该方法是将一个数组进行分割,然后去中间最大值的子数组 def find_max_crossing_subarray(self, array: list, mid: int): # lift = array[:mid] # right = array[mid:] index_lift = mid index_right = mid+1 # 统计左侧数组最大值 lift_max = -sys.maxsize - 1 # 求和 temp = 0 # 记录当前索引 li = 0 while index_lift >= 0: temp += array[index_lift] if lift_max < temp: lift_max = temp li = index_lift index_lift -= 1 t = lift_max lift_max = -sys.maxsize - 1 # 记录右边的索引 temp = 0 ri = 0 while index_right < len(array): temp += array[index_right] if lift_max < temp: lift_max = temp ri = index_right index_right += 1 return array[li:ri + 1], t + lift_max def find_max_subarray(self, array: list): if len(array) <= 1: return array, array[0] mid = len(array) // 2 array_lift, m_lift = self.find_max_subarray(array[:mid]) array_right, m_right = self.find_max_subarray(array[mid:]) # 再调用find_max_crossing_subarray函数,求中间最大值 array_mid, m_mid = self.find_max_crossing_subarray(array, mid) if m_lift > m_right: if m_lift > m_mid: return array_lift, m_lift else: return array_mid, m_mid else: if m_right > m_mid: return array_right, m_right else: return array_mid, m_mid
四、图论之并查集思想
并查集思想是解决图论相关问题。并查集大致可以分为三个步骤,第一步是节点的初始化(一般是利用与节点数量相同的数组,该数组的索引就代表节点,初始化数组为该索引值代表指向自己为根节点,也就是说数组元素存储的值为这个节点的父节点),第二步是构造查找根节点函数,该函数可以使用递归调用实现,也可以利用循环实现。查找根节点主要是查找当前这个节点所在的连通量的根节点。第三步是构建合并函数,根据查找根节点函数判断两个节点是否相同的根节点,如果是相同的根节点则不能合并,不是相同的根节点就可以合并。
背景:假设有一个树形的数据结构,如图所示
根据该图我们可以将树形结构抽象为一个无向图[[1,2],[2,3],[3,4],[1,4],[1,5]]
我们怎么利用并查集去判断树形图是否有环。
首先初始化一个数组parent,此数组的大小为节点数量的大小,初始各个节点的父节点为自己,parent = [0,1,2,3,4,5]
定义一个查找根节点的函数,该函数的思路为,判断parent[index]是否相等index,如果相等则是父节点,如果不是将index赋值为parent[index]循环该步骤,直到parent[index]相等index。返回parent[index]的值
定义合并函数,根据判断查找根节点函数的返回值,如果相同的根节点就合并成功返回true,不同直接返回false。
#python实现 class Solution: def find(self, parent: list, index: int) -> int: while parent[index] != index: index = parent[index] return parent[index] def union(self, parent: list, index1: int, index2: int) -> int: a = self.find(parent,index1) b = self.find(parent,index2) if a != b: parent[a] = self.find(parent, b) return 1 else: return 0 def findRedundantConnection(self, edges: list) -> list: parent = list(range(len(edges) + 1)) for i, j in edges: if self.union(parent, i, j): continue else: return [i, j]
五、二分查找法
1.简述:二分查找法是在一个已经排序完成的数组中使用的查找方法,它的基本思路是:(1)前提:设置前后两个指针指向数组的头节点(low)和尾节点(hight),并且定义中间节点(mid=(low+hight)/2)。(2)步骤:将要查询的节点值与中间节点进行比较,比较情况有三种:①若比中间值大,则调整low=mid+1,mid=(low+hight)/2。②若与中间值相等,则查找完成。③若比中间值小,则调整hight=mid-1,mid=(low+hight)/2。
2.算法框架和分析
以python为例
#以python为例 def twodiv(arr:list,target:int): low = 0 hight = len(arr) mid = (low+hight)//2 while low<hight: if arr[mid] == target: return mid elif arr[mid] < target: low = mid + 1 mid = (low+hight) // 2 else: hight = mid - 1 mid = (low+hight) // 2
六、动态规划
1.简述:动态规划思想就是拒绝重复,也可以说是根据之前搜寻子问题的结果去解决父问题。用动态规划解决问题,该问题就应该有一下特征:①最优子结构②重叠的子问题③状态的转移④边界条件。
最优的子问题也就是说该问题可以划分多个最优的子问题,通过一定的条件进行叠加最优子问题就可以解决该问题。
重叠的子问题:一般能用动态规划解决的问题,都重复进行解决子问题,动态规划其中的一个优点就是可以记录子问题,降低时间复杂度。
状态转移:通过解决子问题,必须通过一定的方法进行迁移,一般是能找到迁移公式,将子问题的结果迁移到父问题,逐步解决了父问题。
边界条件:在进行状态转移的时候会有边界条件需要考虑。
2.动态规划的案例
(1)斐波那契数列问题
分析:当解决斐波那契数列的第n个值,可以把先解决斐波那契数列的第n-1个值和第n-2个值,该问题的转移方程可以很容易的发现f(n) = f(n-1) + f(n-2),它的边界可以是1,1开头。因此该问题可以用动态规划进行解决。
def fei(self, n: int): if n <= 0: return 0 if n == 1 or n == 2: return 1 pre1 = 1 pre2 = 1 nownumber = 0 for i in range(3, n + 1): nownumber = pre1 + pre2 pre1 = pre2 pre2 = nownumber return nownumber
(2)基础背包问题
题目描述:有n个物品,他们有各自的质量和价值。现有给定容量的背包,求如何能让背包里装入的物品具有最大的价值总和,并输出最大价值总和的值。
示例:
#物品重量 weight = [1,3,2,5] #物品价值 value = [20,24,14,15] #背包重量 max_weight = 5
分析:背包重量最大承重是5,首先解决背包容量为1的时候可以存放最大价值的物品,再考虑容量为2的时候。依次递推。边界可以以零容量开始。为了更好的分析可以使用二维数组表格进行分析。如图:
定义一个二维表格,首先更新第i行(从1行开始因为初始0行为零),i行代表考虑前i个物品,j列考虑的是装下总共为j重量的物品。当j重量大于value[i]的时候就考虑装下该物品,否则不考虑,按照i-1行j列进行。
更新规则是:
①当j<weight[i]时 d[i][j] = d[i-1][j]
②当j>=weight[i]时 d[i][j] = max(d[i-1][j],value[i]+d[i-1][j-weight[i]])
七、深度优先搜索
1.简述:
深度优先搜索(DFS)是一种常见的遍历方式,DFS的基本思想是沿着当前查找路径深层遍历,直到结束当前路径的查询,然后返回到之前的位置继续进行没有查找的节点或元素。与DFS思想类似的思想是回溯,递归。DFS的实现是靠栈进行实现的。虽然在一些深度优先搜索的案例中的实现是使用递归进行实现,其实递归就是系统调用的隐形栈。
2.深度优先搜索的实现
深度优先搜索的实现可以与回溯算法类似,与回溯算法不同是回溯算法需要进行搜索之后恢复原来状态,例如我们在一个树种搜索想要的节点,则必须递归和标记,递归则是让DFS进行下去,标记则是减少重复的计算。
深度优先搜索的模板可以用以下伪代码:
#搜索回溯算法模板 def (x :int): if 到达目的地: 输出解 return for i in range(方案数): if 方案可行: 保存路径 if dfs(x+1): return return
3.深度优先搜索的案例
(1)岛屿的数量:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
示例: 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
本题的解决思路可以使用深度优先搜索,利用深度优先搜索可以将岛屿连续找到相连的岛屿,并且标记成'0'(或是其他标记),进行统计执行了几次深度优先遍历,就可以统计出来有多少个岛屿。
#python class Solution: def numIslands(self, grid: List[List[str]]) -> int: number = 0#统计岛屿个数 def dfs(grid, i ,j):#定义递归函数 if i >= len(grid) or j>=len(grid[0]) or i<0 or j<0:#排除条件 return if grid[i][j] == '0': return if grid[i][j] == '1':#进行递归 grid[i][j] = '0' dfs(grid,i-1,j) dfs(grid,i+1,j) dfs(grid,i,j-1) dfs(grid,i,j+1) for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': dfs(grid,i,j) number += 1 else: continue return number
(2)克隆图:有一个无向图,请克隆该无向图。