算法思想

一、前言

从考研结束就一直学习算法和数据结构。经过几个月的学习是时候写一些总结了。程序是由数据结构和算法组成的,数据结构的介绍在另一篇文章上,这里不再过多的赘述。这篇文章主要进行算法思想的总结。算法思想主要包括:分而治之、贪婪算法、动态规划、回溯、哈希表等等。为了便于说明这些思想,这篇文章举例相关的题目进行解说。
图片说明

二、排序

排序有多种方法,常见的有冒泡排序、选择排序、插入排序、快速排序、堆排序等。
(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)克隆图:有一个无向图,请克隆该无向图。