第一阶段-基础算法

二分法

  • 定义:只要能正确淘汰左/右测,就可以二分(经常用单调性,如果左右中三个端点相同,则左端点往右移)
  • 为什么能二分?
    • 1、数据状况特殊
    • 2、问题本身特殊
    • 只要能构造出具有排他性的特性,左侧无,右侧有,就行
  • 常用操作:找到>=某个数的最左侧位置
  • 进阶: [[局部最小值]] http://t.csdn.cn/PXlkx

位运算

  • 异或(xor):无进位的加法
    • 0^N=N
    • N^N=0

位运算

  • [[不用额外变量交换两个数的值]]
    • arr[i] = arr[i] ^ arr[j];
      arr[j] = arr[i] ^ arr[j];
      arr[i] = arr[i] ^ arr[j];
    • 注意:要两数内存地址不同时才行。内存地址相同会被刷成0 -
  • [[数组中一个数出现奇数次,其他数出现偶数次。找出奇数次的数]]
    - 哈希表做词频统计
    - 用eor过一遍,最后留着的就是奇数次的数
  • [[提取出int类型的数的最右的一]]
    - int rightOne = eor & (~eor + 1); // 提取出最右的1
  • [[区分数组中的奇数个x和偶数个y]]
    - 前提:两种数不相等
    - 思路:把两种数分开到两群里,然后各自eor过一遍
    - 操作:把所有数eor过一遍,剩下的就是XeorY,因为X!=Y所以结果不为零。根据结果最右侧1的位置来把原数组的数分成该位有1和该位无1的两群。各组中用eor解决。 [[数组中一个数出现奇数次,其他数出现偶数次。找出奇数次的数]]
  • [[数组中一个数出现k次,其他数出现m次,找出出现k次的数]]
    - 思路:把所有数拆成32位,每个位置累计。则每个位置有四种可能,0,m,k,m+k。
  • 打印一个数的二进制形式:
    • while(aim!=0)
      • now=aim|1
      • aim >> 1
  • [[51、N皇后问题]]
  • [[位图]]
  • 用位运算表示加法:
    • 循环:先无进位加法(eor),再&算出哪些位置可以进位,左移一位后变成下一步的无进位加数。
    • basecase:新的无进位加数为0
  • 减法:等效加上补码
  • 乘法:思路类似进制转换。index位置的1表示2^index值
  • 除法://todo

排序

桶排序

- 原理:按某位的大小依次放桶里(要求稳定性)之后再按桶的顺序倒出,位往左移一位。因为数字高位的权重比低位的权重高,所以低位先进桶排序再排高位,由于高位的后排,所以会打乱低位已经排好的顺序(体现权重)  
  • 原理:桶排序
    用十个计数器标记每个数出现的次数。再根据各个计数器的值去填新数组
    适用范围:0-9排序

public static void countSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   int max = Integer.MIN_VALUE;
   for (int i = 0; i < arr.length; i++) {
      max = Math.max(max, arr[i]);
   }
   int[] bucket = new int[max + 1];
   for (int i = 0; i < arr.length; i++) {
      bucket[arr[i]]++;
   }
   int i = 0;
   for (int j = 0; j < bucket.length; j++) {
      while (bucket[j]-- > 0) {
         arr[i++] = j;
      }
   }
}

特殊化:基数排序
分治的思想:对于数组整体根据标志位(某位上的数)判断进哪个桶。(这调整部分顺序)。对于同一个桶内按原数组的相对位置排列(这部分不变)
private static int[] raduxSort2(int[] arr, int left, int right, int digit) {
    final int num = 10;
    int[] help = new int[right - left + 1];

    //这是出入桶的次数,也是本次排大小的数位
    //basecase:从第一位开始比较,循环比到最高位
    for (int d = 1; d <= digit; d++) {
        int[] count = new int[num];
        //这是每次遍历算某位数出现的次数,为了下文能处理后推算出该放置的index
        for (int i = 0; i < arr.length; i++) {
            int dNum = getNumByDigit(arr[i], d);
            count[dNum]++;
        }
        //遍历处理count数组,原本数组储存的是某位数出现的次数,现在变成某位的数该放置的index
        for (int i = 1; i < count.length; i++) {
            count[i] += count[i - 1];
        }
        //把arr放入help1的相应位置
        for (int i = arr.length-1; i >= 0; i--) {
            int dNum = getNumByDigit(arr[i], d);
            count[dNum]--;
            help[count[dNum]] = arr[i];


        }
        //把help再倒回arr
        for (int i = 0; i < arr.length; i++) {
            arr[i] = help[i];

        }
    }
    return arr;
}

归并排序

  • 原理:整体是递归,左边排好序,右边再排,然后merge排左右
  • 复杂度:merge过程中指针不后退利用了之前排序的结果 O(N*logN)
  • 因为每次比较结果都再次传递,所以比N^2的快
// 请把arr[L..R]排有序
// l...r N
// T(N) = 2 * T(N / 2) + O(N)
// O(N * logN)
public static void process(int[] arr, int L, int R) {
  if (L == R) { // base case
      return;
  }
  int mid = L + ((R - L) >> 1);
  process(arr, L, mid);
  process(arr, mid + 1, R);
  merge(arr, L, mid, R);
}

public static void merge(int[] arr, int L, int M, int R) {
  int[] help = new int[R - L + 1];
  int i = 0;
  int p1 = L;
  int p2 = M + 1;
  while (p1 <= M && p2 <= R) {
      help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
  }
  // 要么p1越界了,要么p2越界了
  while (p1 <= M) {
      help[i++] = arr[p1++];
  }
  while (p2 <= R) {
      help[i++] = arr[p2++];
  }
  for (i = 0; i < help.length; i++) {
      arr[L + i] = help[i];
  }
}

非递归的归并排序

  • 不用建栈模拟递归。而使“宽度拆分”
    因为原结构是数组所以可以在第一层就看到所有元素。可以不断调整操作的覆盖范围。如果是链表结果就不能调整操作元素的范围。
  • 过程:每次循环都增加覆盖范围,用这个范围遍历整个数组。每次都只merge覆盖范围内的两段数组
    特点:用步长(范围)替换了递归取L和R
    代码
// 非递归方法实现
public static void mergeSort2(int[] arr) {
  if (arr == null || arr.length < 2) {
      return;
  }
  int N = arr.length;
  // 步长
  int mergeSize = 1;
  while (mergeSize < N) { // log N
      // 当前左组的,第一个位置
      int L = 0;
      //每个步长都遍历一遍数组
      while (L < N) {
          //防止M越界,M越界的时候就可以下阶段的步长了
          //确定LMR的值并且防止越界
          //确定完值就传给merge
          if (mergeSize >= N - L) {
              break;
          }
          int M = L + mergeSize - 1;
          int R = M + Math.min(mergeSize, N - M - 1);
          merge(arr, L, M, R);
          //再推到下一组
          L = R + 1;
      }
      // 防止溢出
      if (mergeSize > N / 2) {
          break;
      }
      mergeSize <<= 1;
  }
}

快排

basecase:左右指针碰撞
随机一个位置的数和最后互换,拿最后的数和数组其他位置的数组对比,利用左右指针的移动划分大,小区。接着递归处理0L指针,Rlen-1的两区间
(非递归版本就是手动压栈,在函数中用while循环pop栈,本来调用递归的地方push栈)

// 快排3.0 非递归版本
public static void quickSort2(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   int N = arr.length;
   //先随机打到某个位置再和结尾互换,之后就是正常快排的过程
   swap(arr, (int) (Math.random() * N), N - 1);
   //这里获取区间下标
   int[] equalArea = netherlandsFlag(arr, 0, N - 1);
   int el = equalArea[0];
   int er = equalArea[1];
   //手动调用栈
   Stack<Op> stack = new Stack<>();
   //把两项任务压入栈中
   stack.push(new Op(0, el - 1));
   stack.push(new Op(er + 1, N - 1));
   while (!stack.isEmpty()) {
      Op op = stack.pop(); // op.l  ... op.r
      if (op.l < op.r) {
         swap(arr, op.l + (int) (Math.random() * (op.r - op.l + 1)), op.r);
         equalArea = netherlandsFlag(arr, op.l, op.r);
         el = equalArea[0];
         er = equalArea[1];
         stack.push(new Op(op.l, el - 1));
         stack.push(new Op(er + 1, op.r));
      }
   }
}

bfprt

问题:无序数组中查找第k小
跟快排的区别:快排是随机选一个作为分度值的数,bfprt是讲究的选一个中位数
中位数挑选的过程:
向把数组中每五个数分成一组
每个组排序再提出中位数
一共N/5个中位数再提取出中位数。
此时的中位数一定≥3/10N的数,也一定≤3/10N的数
所以每次排序最差结果也能排除3/10N的数
在这里插入图片描述

[[递归]]

  • 暴力递归(尝试)的原则
    • 把问题转换为缩小规模的同类子问题=》 [[分治]]
      • 超长数组的背包问题,分成左右两组各自求背包再合并数值
    • 明确的停止递归条件(base case)
    • 得到子问题(子阶段)的结果后的决策条件和过程
    • 不记录每个子问题的解=》记录就是dp
  • 流程:大问题拆成小问题,直到问题规模足够小到能解决时base case,得到结果返回给上级回溯。普遍级别需要收集下层返回的信息再进行加工后提供给上层决策。
  • 黑盒思维:规定好上层给的输入,下层返回的输出,限制条件,base case后直接使用黑盒执行决策过程
  • 递归行为:
    • master公式
    • 汉诺塔
    • [[打印字符串的全部子序列]]
    • [[打印字符串的全排列]]
      – 思路:char[]每位选择一个字母填上(用数组做欠债表是通解)
    • 参数,i:已经填完之前的位置。path:目前填的结果。map:剩余可用字母
    • basecase:i=str.length
    • 核心:dfs+恢复现场
  • [[使用递归不额外申请空间逆序栈]]//todo
    • 思路:创建一个函数popFromBottom()弹出栈底元素。
      在主函数递归popFromBottom()获取栈底元素,栈为空后再push之前获取的栈底元素。
    • popFromBottom():弹出栈底元素。
      原理:每层都移除当前栈顶元素并且返回下层的返回值,再把当前栈顶push回去。
      basecase,pop后为空就返回pop值(原栈底)
    • 主函数是从下到上取元素,所以push回去的时候元素变为倒序
    • 每个层数都调用一次reverse。调用这次reverse->popFromBottom方法会递归层数以取出栈底
    • 例:5层时reverse->popFromBottom调用5次->4层reverse->popFromBottom调用4次…
    • 在这里插入图片描述

[[动态规划]]

  • 问题:什么样的递归可以优化?
    • 答案:有重复调用同一个子问题的递归。//比如传入的参数相同
    • (如果每个子问题都是不同的解,不用也无法优化。例如[[51.N皇后问题]])
  • 问题:暴力递归和动态规划的关系?
    • 答案:动态规划是暴力递归的真子集
  • 问题:题目和动态规划的关系?
    • 答案:一题多解
  • 问题:做出动态规划的步骤
    • 答案:
      ①先整暴力递归。注意重要原则和4种常见尝试模型
      ②分析存不存在重复的小问题=》为了改记忆法搜索
      流程:操作函数先从缓存中取值,没有才计算。计算完建结果放入缓存中
      ③转化为严格的表结构
      先找basecase,边界条件填上表中的一些位置。再找普遍位置的依赖情况。按依赖的反向填表
      ④对表结构的依赖进行优化
      滑动窗口
      平行四边形
      斜率优化
  • 问题:设计暴力递归过程的原则有哪些?
    • 答案:
      ①每个入参不要超过int类型
      ②如果违反了①则入参只能是一维线性结构。(优化到记忆法搜索即可)
      ③几个可变参数对应几维的表
  • 问题:如何根据设计原则做题?
    • 答案:只考虑在原则范围内的暴力尝试,不行就换(极少超纲)
  • 问题:暴力递归改动态规划的流程?
    • ①先有一个不违反原则的暴力递归,且存在子问题的重复调用
      ②挑选合适的入参且列出变化范围
      ③参数间所有的笛卡尔积为表的大小
      ④可以改记忆法搜索
      (没有枚举行为不用继续)
      ⑤构建严格的表结构,分析一般位置的表依赖。依赖相反顺序填表
      ⑥有枚举行为则可以进一步优化
  • 问题:动态规划的进一步优化?
    • 答案:
      ①空间压缩
      ②状态化简
      ③dp中的四边形不等式优化
      在这里插入图片描述
  • [[从左到右的尝试模型]]
    • [[背包问题]]
    • [[数字转字母串]]
    • [[691. 贴纸拼词]]#欠账表 #词频
    • [[货币凑面值类题目]]#dp枚举优化
    • [[把正数数组分成两个size相等,累加和最接近的集合]]#三维dp
  • [[范围尝试模型]]
    • [[两玩家从左右两端取纸牌,求获胜者分数]]
    • [[516.最长回文子序列]]
  • [[样本对应模型]]
    • [[求两字符串的最长公共子序列]]
    • [[最短路径和问题]] #空间压缩技巧
    • [[K步超出矩阵概率]]
    • [[人杀死怪兽的概率]]
    • [[给定一个正数求裂开的方法数]]
  • [[业务限制模型]]
    • [[象棋跳马问题]]
    • [[k步到达指定坐标]]
    • [[洗咖啡杯问题]]
    • [[51.N皇后问题]] #位运算
  • TODO [[四边形不等式技巧]]
  • TODO [[状态压缩的动态规划]]
  • TODO [[动态规划的外部信息简化]]

[[贪心]]

  • 贪心含义:
    - 1、自然智慧
    - 2、每次都选择局部最优解
    - 3、证明局部最优解就是全局最优解
  • 贪心标准过程:
    • 分析业务限制
    • 根据业务逻辑找不同的贪心策略
    • 对能举出反例的情况直接跳过
  • 贪心解题过程:
    • 先写出不依靠贪心的解法
    • 写贪心解
    • 用对数器验证,不用数学验证
    • 堆,排序,是贪心常用的工具。堆:根据比较器的排序原则把顶端的数弹出,就有贪心的意思
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值