美文网首页
基础算法总结

基础算法总结

作者: voidFan | 来源:发表于2021-03-03 22:46 被阅读0次

    一、链表问题

    链表问题一定要进行举例画图,辅助思考!
    使用快慢指针遍历链表。因为链表无法得知长度,所以尝试用这种方法来达到某种效果(链表中点,检测环等)。
    警惕指针丢失。可以使用一些临时变量来存储next指针,在链表插入节点时应先连接后边节点,避免断链表造成指针丢失。
    设置虚拟节点(哨兵),简化实现难度。对于插入和删除等操作,往往需要一个额外的指针来记录其前面的节点(前驱节点)。
    单链表递归实现。对一些依赖于后面节点才可以完成的操作,使用递归的方式来解决。
    注意留意常见的边界条件处理:链表为空,链表有一个或者两个节点,头结点(例如旋转链表)和尾节点。

    环问题

    1、141判断链表有环:【快慢指针】
    2、环的长度:【快慢指针判断有还,并记录相交的点,再遍历计数计算环入口/环长度】
    3、142环的入口节点:【快慢指针,再从头遍历链表】
    4、287寻找重复数:弗洛伊德算法找环入口

    重构链表

    1、206反转链表:【迭代 递归】【1->2->3->∅】 --->> 【∅<-1<-2<-3】

    链表反转.jpg 链表反转(递归实现).jpg

    2、92反转链表II:
    3、24交换链表中的相邻节点
    4、143重排链表
    5、61旋转链表

    拆分/组合

    1、86分隔链表:
    2、21归并两个有序链表:
    3、链表求和
    4、两链表的交点
    5、排序链表:【147插入、148归并、快速】

    删除类型

    1、83删除已排好序链表的重复元素
    2、203删除链表元素

    func removeElements(head *ListNode, val int) *ListNode {
        if head == nil {
            return nil
        }
        dummnyhead := &ListNode{
            Val:-1,
            Next:head,  // 创建一个指向head的虚拟头节点
        }               // 否则,第一个节点的删除会有问题
        curr := dummnyhead
        for curr.Next != nil {
            if curr.Next.Val == val {
                curr.Next = curr.Next.Next
            } else {
                curr = curr.Next
            }
        }
        return dummnyhead.Next
    }
    

    3、19删除链表的倒数第 n 个节点

    二、dp动态规划

    动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。下面我们先来讲下做动态规划题很重要的三个步骤,

    第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?

    第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2].....dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。

    最优子结构,把大的问题拆分成小的问题。
    第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。

    由了初始值,并且有了数组元素之间的关系式,那么我们就可以得到 dp[n] 的值了,而 dp[n] 的含义是由你来定义的,你想求什么,就定义它是什么,这样,这道题也就解出来了。

    二、直接上leetcode真题

    • LC2、无重复字符的最长子串

      int lengthOfLongestSubstring(string s) {
          int start = 0;  // 1、当前处理的子串,看看子串,是否有重复的字符
          int max = 0;
      
          unordered_map<char, int> hash;  //用来记录 当前字符的 下标位置。
          for (int end = 0; end < s.size(); end++) {
              char tmp = s[end];
              // 2、从start 到 end 子串 如果有重复字符,需要更新start
              if ( hash.find(tmp) != hash.end() && hash[tmp] >= start) {
                  start = hash[tmp] + 1;
              }
              hash[tmp] = end;
              // 3、查看最大长度,是否更新.  start 到 end  
              if(end - start + 1 > max) {
                  max = end - start + 1;
              }   
          }
          return max;
      }
      
    • LC5、最长回文子串

      string longestPalindrome(string s) {
              int len = s.size();
              if (len == 0) return s;
          
              bool dp[len][len];
              //定义一个二维数组bool dp[len-1][len-1]来记录遍历字符串所得的状态,
              //  dp[left][right]为true表示"从left到right的子串为回文串",false表示不是回文串
              int start = 0, end = 0;
              //初始化二维数组,单个字符为回文串,所以定义dp[i][i] = true
              for (int i = 0; i <len; i++)
                  dp[i][i] = true;
              for (int right = 1; right < len; right++)
                  for (int left = 0; left < right; left++)
      
                      if (s[right]==s[left] && (right-left==1 || dp[left+1][right-1])) {
             //dp[left][right] = 
             //(s[right]==s[left] && (right-left==1 || dp[left+1][right-1])) ? 
             //                   true : false
             // s[right]==s[left] && (right-left==1)       情况一:bb 这种
             // s[right]==s[left] && dp[left+1][right-1]   情况二:bab 
             //  如果我们已经知道 “bab” 是回文,那么很明显,“ababa” 一定是回文,
             //   因为它的左首字母和右尾字母是相同的。
                          dp[left][right] = true;
                          if (right-left > end-start) {
                              start = left; 
                              end = right;
                          }
                          continue;
                      } else {
                          dp[left][right] = false;
                      }     
              return s.substr(start, end-start+1);
          }
      
    • LC53、连续子串和最大

      int maxSubArray(int* nums, int numsSize){
          //1、定义dp[i] 以第i个数字结尾的连续子串的和。作为状态 
          int *dp = NULL;
          dp = (int *)malloc(sizeof(int) * numsSize);
          memset(dp, 0, sizeof(int) * numsSize);
          //2、初始化零界值
          dp[0] = nums[0];
      
          for(int i = 1; i < numsSize; i++){
              dp[i] = (dp[i-1] + nums[i] > nums[i]) ? (dp[i-1] + nums[i]) : nums[i];    
              // max(dp[i-1] + nums[i], nums[i]);
          }
          int maxNum = dp[0];
          for(int j = 0; j < numsSize; j++){
              if(dp[j] > maxNum)
                  maxNum = dp[j];
          }
          return maxNum;
      }
      
      int maxNum(int a, int b){
          if (a >= b)
              return a;
          else
              return b;
      }
      int maxSubArray(vector<int>& nums) {
          int sum = 0x80000001;    //最小的32位整数
          int max = 0;
          for (int i = 0; i < nums.size(); i++) {
              max = maxNum(max + nums[i], nums[i]);
              if (max > sum) {
                  sum = max;
              }
          }
          return sum;
      }
      
    • LC64、编辑距离

      /*
      对一个单词进行如下三种操作:插入一个字符\删除一个字符\替换一个字符
      将 word1 转换成 word2 所使用的最少操作数 。
      case:
      输入:word1 = "intention", word2 = "execution"
      输出:5
      解释:
      intention -> inention (删除 't')
      inention -> enention (将 'i' 替换为 'e')
      enention -> exention (将 'n' 替换为 'x')
      exention -> exection (将 'n' 替换为 'c')
      exection -> execution (插入 'u')
      */
      int minDistance(string word1, string word2) {
          int size1 = word1.size();
          int size2 = word2.size();
          // int dp[i][j] 表示字符串 word1 的长度为 i,字符串 word2 的长度为 j 时,
          //              将 word1 转化为 word2 所使用的最少操作次数为 dp[i] [j]。
          //1、插入一个字符
          //2、删除一个字符
          //3、替换一个字符
          int dp[size1+1][size2+1] = {0};
          for(int i = 1; i <= size1; i++){
              dp[i][0] = i;
          }
          for(int j = 1; j <= size2; j++){
              dp[0][j] = j;
          }
      
          for(int i = 1; i <= size1; i++){
              for(int j = 1; j <= size2; j++){
                  // // 关系一:
                  // if(word1[i] == word2[j]){
                  //     dp[i][j] = dp[i-1][j-1];
                  // }else{
                  //     关系二: dp[i][j-1]表示在word1尾部插入一个字母, 
                  //     dp[i-1][j]表示在word1尾部删除一个字母,(相当于在word2尾部插入一个字母),
                  //     dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
                  // }
                  // // 关系一:
                  dp[i][j] = min(dp[i][j-1], dp[i-1][j])+1;                                           //插入 删除 时
                  dp[i][j] = min(dp[i][j], dp[i-1][j-1] + (word1[i-1]==word2[j-1] ? 0:1));             //替换时
              }
          }
          return dp[size1][size2];
      }
      
    • LC152、乘积最大子序列

      int maxProduct(vector<int>& nums) {
          // 1、dp[i] : 表示以以下标i结束的子序列,的最大值和最小值。
          vector<int> dpMax(nums.size(), 0);
          vector<int> dpMin(nums.size(), 0);
      
          //2、初始化边界
          dpMax[0] = nums[0];
          dpMin[0] = nums[0];
          int ans = nums[0];
          for(int i = 1; i < nums.size(); i++){
              dpMax[i] = (dpMax[i-1] * nums[i] >= dpMin[i-1] * nums[i]) ? dpMax[i-1] * nums[i] : dpMin[i-1] * nums[i];
              dpMax[i] = dpMax[i] >= nums[i] ? dpMax[i] : nums[i];
      
              dpMin[i] = (dpMax[i-1] * nums[i] < dpMin[i-1] * nums[i]) ? dpMax[i-1] * nums[i] : dpMin[i-1] * nums[i];
              dpMin[i] = dpMin[i] < nums[i] ? dpMin[i] : nums[i];
              if(ans < dpMax[i]){
                  ans = dpMax[i];
              }
          }
          return ans;
      }
      
    • LC673、最长递增子序列的个数

      int findNumberOfLIS(vector<int>& nums) {
          int n = nums.size();
          if(n == 0){
              return  0;
          }
          int LIS = 1;
          vector<pair<int, int> > dp(n, {1, 1});
          for (int i = 1; i < n; ++i) {
              for (int j = 0; j < i; ++j) {
                  if (nums[i] > nums[j]) {
                      if (dp[i].first < dp[j].first + 1) {
                          dp[i] = {dp[j].first + 1, dp[j].second};
                      } else if (dp[i].first == dp[j].first + 1) {
                          dp[i].second += dp[j].second;
                      }
                  }
              }
              if(LIS < dp[i].first){
                  LIS = dp[i].first;
              }
          }
          int res = 0;
          for (int i = 0; i < n; ++i) {
              if (dp[i].first == LIS) {
                  res += dp[i].second;
              }
          }
          return res;
      }
      
    • LC300、最长递增子序列<<给你一个整数数组 nums ,找到其中最长严格递增子序列的长度>>。

      int lengthOfLIS(vector<int>& nums) {
          int size = nums.size();
          if(size < 2){
              return size;
          }
          int maxLen = 1;
          vector<int> dp(size, 1);  //1、 DP[i] 定义以i为结尾的最长上升子序列的长度
          // 2、从第二个元素开始处理
          for(int i = 1; i < size; i++){
              for(int j = 0; j < i; j++){
                  if(nums[i] > nums[j] && dp[i] < dp[j] + 1){
                      dp[i] = dp[j] + 1;
                  }
              }
              if(maxLen < dp[i]){
                  maxLen = dp[i];
              }
          }
      
          return maxLen;
      }
      
    • LC120、三角形最小路径和

      /*
      输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
      输出:11
      解释:如下面简图所示:
         2
        3 4
       6 5 7
      4 1 8 3
      自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
      */
      int minimumTotal(vector<vector<int>>& triangle) {
          int m = triangle.size();
          if(m == 0) {
              return 0;
          }
          int dp[m] = {0}; 
          //1. 动态规划:构造一个三角形 dp记录当前行从上往下的和
          //每个元素只能走到它的下方或者右下方
          //也就是每个元素只能来自它的上方或者左上方
          //注意特殊情况:左腰上的元素,只能来自它的上方;右腰只能从左上方来。
          dp[0] = triangle[0][0];     // 2. 边界处理
          for(int i = 1; i < m; i++){           // 依次处理每一行,
              dp[i] = triangle[i][i] + dp[i-1]; // 每一行的第一个元素。
              for(int j = i - 1; j > 0; j--)  //为了避免数据覆盖,从后往前填充
                  dp[j] = triangle[i][j] + min(dp[j-1], dp[j]);
              dp[0] += triangle[i][0];
          }
          int ans = dp[0];
          for(int i = 1; i < m; i++)
              ans = min(dp[i], ans);
          return ans;
      }
      
    • LC121、卖买股票的最佳时机

      /*
      输入:[7,1,5,3,6,4]
      输出:5
      解释:在第 2 天(股票价格 = 1)的时候买入,
           在第 5 天(股票价格 = 6)的时候卖出,
           最大利润 = 6-1 = 5 。
           注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
      */
      /*
      思路还是挺清晰的,还是DP思想:
      记录【今天之前买入的最小值】
      计算【今天之前最小值买入,今天卖出的获利】,也即【今天卖出的最大获利】
      比较【每天的最大获利】,取最大值即可
      */
      int maxProfit(vector<int>& prices) {
          int dayNum = prices.size();
          if(dayNum <= 0){
              return 0;
          }
          int buyIn = prices[0];           // 定义一个最值,做为买入股票的最佳时机。
          int max = 0;                     // 定义为最大的收益
          for(int i = 1; i < dayNum; i++){
              if(buyIn > prices[i - 1]){    // 买入价如果不是最大值的话,要更新买入价
                  buyIn = prices[i - 1];
              }
              if(prices[i] - buyIn > max){  // 收入最大化
                  max = prices[i] - buyIn;
              }
          }
          return max;
      }
      
    • LC128 最长连续序列

      /*
      输入:nums = [100,4,200,1,3,2]
      输出:4
      解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
      */
      int longestConsecutive(vector<int>& nums) {
          if (nums.size() < 2) {
              return nums.size();
          }
          sort(nums.begin(), nums.end());                              // 先排序
          nums.erase(unique(nums.begin(), nums.end()), nums.end());    // 去掉重复元素
      
          int res = 1;
          int n = nums.size();
          vector<int> dp(n, 1);
          // dp 问题: dp[i] 表示以当前元素结尾的连续序列的长度。
          for (int i = 1; i < n; i++) {
              if (nums[i-1] + 1 == nums[i]) {                         
                  dp[i] = dp[i-1] + 1;
              }
              res = max(res, dp[i]);
          }
          return res;
      }
      
    // 最终结果
    var result [][]int
    
    // 回溯核心
    // nums: 原始列表
    // pathNums: 路径上的数字
    // used: 是否访问过
    func backtrack(nums, pathNums []int, used[]bool) {
        // 结束条件:走完了,也就是路径上的数字总数等于原始列表总数
        if len(nums) == len(pathNums) {
            tmp := make([]int, len(nums))
            // 切片底层公用数据,所以要copy
            copy(tmp, pathNums)
            // 把本次结果追加到最终结果上
            result = append(result, tmp)
            return
        }
    
        // 开始遍历原始数组的每个数字
        for i:=0; i<len(nums); i++ {
            // 检查是否访问过
            if !used[i] {
                // 没有访问过就选择它,然后标记成已访问过的
                used[i] = true
                // 做选择:将这个数字加入到路径的尾部,这里用数组模拟链表
                pathNums = append(pathNums, nums[i])
                backtrack(nums,pathNums,used)
                // 撤销刚才的选择,也就是恢复操作
                pathNums = pathNums[:len(pathNums) -1]
                // 标记成未使用
                used[i] = false
            }
        }
    }
    
    func permute(nums []int) [][]int {
        var pathNums []int
        var used = make([]bool, len(nums))
        // 清空全局数组(leetcode多次执行全局变量不会消失)
        result = [][]int{}
        backtrack(nums, pathNums, used)
        return result
    }
    
    class Solution {
    public:
        vector<vector<int>> permute(vector<int>& nums) {
            vector<vector<int>> result;
            backtrack(nums, result, 0);  //从第一个开始处理
            return result;
        }
        void backtrack(vector<int> & nums, vector<vector<int>> & result, int loc){
            if (loc == nums.size()) {
                result.push_back(nums);   //所有数据都处理完成,退出
                return;
            }
            // 从下标位置0 开始处理到 size - 1    loc下标是nums的指针
            //                                   i 下标零时一个排列的下标
            for (int i = loc; i < nums.size(); i++) {
                if (loc != i) {
                    swap(nums[i], nums[loc]);    //使用交换的话,就不用标记这个元素使用过了
                }
                backtrack(nums, result, loc + 1);
    
                if (loc != i) {
                    swap(nums[loc], nums[i]);
                }
            }
        }
    };
    

    判断一颗树是否是平衡二叉树

    题思路
    后续遍历+DFS

    dfs计算思路:

    对于空结点,深度为0
    当前深度是左右子树深度的最大值+1, 有效情况直接返回深度
    一旦发现左右子树的深度差异超过1,则认为无效,返回-1
    一旦发现返回是-1, 直接返回-1

    /**
     * Definition for a binary tree node.
     * struct TreeNode {
     *     int val;
     *     TreeNode *left;
     *     TreeNode *right;
     *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
     * };
     */
    class Solution {
    public:
        bool isBalanced(TreeNode* root) {
            return dfs(root) != -1;
        }
    
        int dfs(TreeNode* node)
        {
            if (node != nullptr)
            {
                int left = dfs(node->left);
                if (left == -1)
                {
                    return -1;
                }
                int right = dfs(node->right);
                if (right == -1)
                {
                    return -1;
                }
    
                return abs(left-right) > 1 ? -1 : max(left, right) + 1;
            }
            else
            {
                return 0;
            }
        }
    };
    

    相关文章

      网友评论

          本文标题:基础算法总结

          本文链接:https://www.haomeiwen.com/subject/dlzcqltx.html