常见算法

2022-07-16  本文已影响0人  zhemehao819

常见算法

1.链表反转

例:

输入:1 -> 2 -> 3 -> 4 -> 5

输出:5 -> 4 -> 3 -> 2-> 1

解法1:迭代

(1)while循环遍历链表

(2)声明3个变量:curr指向当前遍历节点,next临时存放下一个节点,pre临时存放当前节点(初始值为null)

(3)循环执行如下,结束条件是curr==null;

     -  临时变量next存放下一个节点:next = cure.next;

     -  当前节点的next指向pre:curr.next = pre;

     - 临时变量pre存放当前节点:pre = curr;

     - curr移到下一个节点:cure = next;
public Node iterate(Node head) {
  Node curr = head;
  Node next;
  Node pre = null;
  while (curr != null) {
    next = curr.next;
    curr.next = pre;
    pre = curr;
    curr = next;
  }
  return pre;
}

解法2:递归

(1)递归结束条件是head==null或head.next==null

(2)每个单元操作:

    - 当前节点的下一个节点的next指向当前节点:head.next.next = head;

    - 断开原有指针:head.next = null;
public Node recursion(Node head) {
  //递归结束条件:链表是null或递归到链表的最后一个节点
  if (head == null || head.next == null) {
    return head;//<1>
  }
  // 否则 进行递归操作。对下一个单元调用该方法
  Node newHead = recursion(head.next);//返回的是<1>处的值
  
  //对每个单元操作
  head.next.next = head;
  head.next = null;
  
  return newHead;
}

2.素数个数统计

统计n以内的素数个数

素数:只能被1和自己整除的自然数,0,1 除外

例:输入:100

    输出:25

解法1:暴力法

public int bf(int n) {
  int count = 0;
  for (int i = 2; i < n; i++) {
    if (isSuShu(i)) count++;
  }
  return count;
}

private boolean isSuShu(int n) {
  for (int i = 2; i < n; i++) { // 此处条件判断可以优化为 i*i < n
    if (n % i == 0) {
      return false;
    }
  }
  return true;
}

可以优化一下,如上注释。

解法2:埃筛法

当找出一个素数2时,那么2✖️2,2✖️3,2✖️4,等待都不是素数,后面再循环判断就是浪费,可以用一个数组给标记出来,标记出来的数就不用再判断是否时素数了。

public int esf(int n) {
  boolean[] isHeShu = new boolean[n];
  int count = 0;
  for (int i = 2; i < n; i++) {
    if (!isHeShu[i]) {
      count++;
      for(int j = 2*i; j<n; j+=i) {//i是素数,那么i的倍数都不是素数,标记为true
        isHeShu[j] = true;
      }
    }
  }
  return count;
}

同样可以优化:

i=2时,2✖️2,2✖️3,2✖️4

i=3时,2✖️3,3✖️3,4✖️3

j = 2✖️i可以优化为j = i✖️i

3.删除排序数组中的重复项

一个有序数组nums,原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。
不能使用额外的数组空间,必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。

例:输入:[0,1,2,2,3,3,4]
输出:5

解法:双指针算法

双指针算法:

数组完成排序后,可以用两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针,初始状态j=i+1。

当nums[i] != nums[j] 时,i指针和j指针同时向后移动;

当nums[i] == nums[j],就对j自增以跳过重复项,此时,把nums[j] 的值复制到nums[i+1],即i++;

接着将再次重复相同的过程,直到j到达数组的末尾为止。

public int removeDuplicates (int[] nums){
  if(nums.length==0) return 0;

    int i=0;
  for(int j=1; j<nums.length; j++){
    if(nums[j]!=nums[i]) { //等同于nums[i] == nums[j]时,只往后移动j指针。
            i++;//j已经在for循环中可自增
      nums[i]=nums[j];
    }
    }
    
    return i+1;//新数组长度等于i指针下标 加1;
}

4.寻找数组的中心下标

给定一个整数数组nums,请编写一个能够返回数组“中心下标”的方法。

中心下标是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果数组不存在中心下标,返回-1。如果数组有多个中心下标,应该返回最靠近左边的那一个。
注意:中心下标可能出现在数组的两端。

解法:

先统计出整个数组的总和,然后从第一个元素开始叠加
总和递减当前元素,叠加递增当前元素,直到两个值相等

public int pivotIndex (int[] nums){
  int sum2=Arrays.stream(nums).sum();
  int sum1=0;
  for(int i=0; i<nums.length; i++){
    sum1 += nums[i];
    if(sum1 == sum2){//index=i的元素 sum1和sum2都加了,如果相等,说明i就是中心下标。
        return i;
    }
    // 否则,右侧sum减去nums[i],接着进行下一轮循环。
    sum2 = sum2 - nums[i];
  }
  return −1;
}

5.x的平方根

在不使用sqrt(x)函数的情况下,得到x的平方根的整数部分

解法1: 二分法

属于暴力解法的优化

//二分查找
public int binarySearch (int x){
  //定义最左最右两个数值
  int l = 0;
  int r = x;
  
  int index = -1;//为记住mid值

  while(l <= r){
    int mid = l + (r-l)/2;//l和r之间的中间值等于 右值减去左值,再加上左值
    if(mid * mid <= x){ //说明x的平方根在mid的y右侧
      index = mid;
            l = mid + 1;//最左数变为mid+1
    } else {
      r = mid - l;
    }
  }
  
  return index;
}

解法2: 牛顿送代

等同于 n*n = x ,求n

那么 n = x/n

double res = (n + x/n)/2,然后递归,让res无限接近n,直到等于。

public int newton(int x) {
  if (x == 0) {
    return 0;
  }
  return sqrt(x, x);//第一个参数可以随意取值
}

public double sqrt(double n, int x) {
  double res = (n + x/n)/2;
  if (res == n) {
    return n;
  } else {
    return sqrt(res, x);
  }
}

6.两数之和

给定一个整数数组numbers,从数组中找出两个数满足相加之和等于目标数target。
假设每个输入只对应唯一的答案,而且不可以重复使用相同的元素。

返回两数的下标值,以数组形式返回。

解法1: 暴力解法

使用两个for循环,遍历判断。

优化点:第2层for循环的起始下标可以时i+1

解法2: 使用Map标记

在解法1上优化,x+y=target,那么y=target-x

使用一个for循环找y,对于不满足的放入Map<数,下标>,

后面循环可以先判断这个map中是否已经存在,存在时直接返回相应下标,不存在时放入。

解法3: 有序时,二分法

对于有序数组,可以用二分法,low和high分别表示数组中0号和末尾的数值,

相加后,满足target直接返回;

大于target时,high移动到mid左侧1位,

小于target时,low移动到mid右侧1位,

解法4: 有序时,双指针法

类似二分法,只是用index指针来分别指向最低和最高位,

两位对应值相加大于target时,右边指针左移1位,

小于target时,左侧指针向右移动1位,

直到两位对应值相加值等于target。

7.斐波那契数列

求取斐波那契数列第N位的值。

麦波那契数列:每一位的值等于他前两位数字之和。前
两位固定0,1,1,2,3,5.8。。。。

解法1:暴力递归

public int calculate(int n) {
  if (n == 0 || n == 1) {//前两位固定
    return n;
  }
  //以后每位都等于前两位只和
  return calculate(n-1) + calculate(n-2);
}

解法2:去重递归

对解法1降低时间复杂度

比如计算c(5) = c(4)+c(3) = c(3)+c(2) + c(2)+c(1) = ...

重复计算了c(3)、c(2)、c(1) 等,可以去重,对已经计算过的值进行保存,以后遇到了直接用。

对每位得值存入对应数组中

public int calculate(int n) {
  // 用一个数组来存放对于位置的斐波那列数
  int[] arr = new int[n + 1];
  
  return indexValue(arr, n-1) + indexValue(arr, n-2)
}

// 返回的是对于位值,并对于对应位值为0的下标赋值
private int indexValue(int[] arr, int n) {
  if (n == 0 || n == 1) {
    return n;
  }
  
  if (arr[n] != 0) { //说明对于位已有值,直接返回值即可
    return arr[n];
  }
  
  //还是递归填值
  arr[n] = indexValue(arr, n-1) + indexValue(arr, n-2);
  return arr[n];
}

解法3:双指针迭代

对解法2降低空间复杂度

不需要对每位的斐波那列数都存放在数组里,其实只需要存放两位即可,然后重复利用两位

public int iterate(int n) {
  if (n == 0 || n == 1) {
    return n;
  }
  
  int low = 0;
  int high = 1;
  for (int index = 2; index <= n; index++) {
    int sum = low + high;
    low = high;
    high = sum;
  }
  return high;
}

8.排列硬币

总共有n枚硬币,将它们摆成一个阶梯形状,第k行就必须正好有k枚硬币。
给定一个数字n,找出可形成完整阶梯行的总行数。
n是一个非负整数,并且在32位有符号整型的范围内。

解法1: 暴力解法

每排完一行,计算剩余的硬币个数,看是否排出下一行。

public int getTheRow(int num) {
  for (int row = 1; row <= num; row++) {//第row行放row个硬币,最多肯定不超过num行
    int last = num - row; //放完第row行后还剩下last个硬盘
    num = last; // 把剩余的个数赋值给num
    if (num <= row) { // 如果剩余num数小于或等于当前row,那么说明下一个行(row+1)肯定不能形成一个完整的行
      return row;
    }
  }
  return 0;
}

解法2:二分法

有num个硬盘,排完后最多不超过num行,使用二分法在这num行中找出答案的x行,然后对1-x行的总和应该等于num

public int div(int num) {
  //申明2个边界变量
  int low = 0;
  int high = num; //假设最多就有num行
  
  while(low <= high) {
    int mid = (high - low)2 + low;
    int sum = mid*(mid + 1)/2; // mid行的之和硬币数
    
    if (sum == num) { //如果刚好相等,说明mid这一行就是最后一行,刚刚好
      return mid;
    } else if (sum > num) {//总和大了,high左移到mid左侧一位
      high = mid - 1;
    } else if (sum < num) {// 总和小了,low右移到mid右侧一位
      low = mid + 1;
    }
  }
}

解法3:牛顿迭代

假设答案是x,那么求和, num=(1+x)x/2. ---> x * x = 2 num - x

那么,x = (2 num - x)/x

public double newton(double x, int num) {
  double result = (2 * num - x) / x;
  if (result == x) {
    return result;
  } else {
    return newton(result, num);
  }
}

public int getRowValue(int num) {
  if (num == 0) return 0;
  
  return (int)newton(num, num);
}

9.环形链表

给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next指针再次到达该节点,则链表中存在环
如果链表中存在环,则返回true。否则,返回false

解法1:常规

循环遍历,当遍历到曾经遍历过的节点,说明存在环。那么如何来判断曾经是否遍历过?

可以使用set集合,不能放进去的节点表面存在环。

public boolean isCycle(Node head) {
  Set<Node> set = new HashSet<>();
  while(head != null) {
    if(!set.add(head)) {
      return true;
    }
    head = head.next;
  }
  return false;
}

解法2:双指针法(快慢指针)

优化解法1的空间复杂度。

假设有两个指针,一个快一个慢,如果没环则最后快指针最先指向null;

如果有环,快指针先进入环,慢指针后进入环中,并且它们两肯定会相遇。

public boolean isCycle(Node head) {
  if (head == null || head.next == null) {
    return false;
  }
  
  //声明快慢指针
  Node slow = head;
  Node quick = head.next;
  while(slow != quick) {
    if (quick == null || quick.next == null) {
      return false;
    }
    slow = slow.next; // 慢指针每次走一步
    quick = quick.next.next;//快指针每次走两步
  }
  return true;//说明跳出来while循环,存在环
}

10.合并两个有序数组

两个有序整数数组num1,将num2,将num2合并到num1中,使num1成为一个有序数组。
初始化num1和num2的元素数量分别为m和n。假设num1的空间大小等于m+n,这样它就有足够的空间保存来自num2的元素。

例如:

num1={1,3,5,7,9,0,0,0,0}

num2={2,4,6,8}

解法1:常规法

先合并再排序

public int[] merge(int[] num1, int m, int[] num2, int n) {
  System.arraycopy(num2, 0, num1, m, n);
  Arrays.sort(num1);
  return num1;
}

解法2:双指针法

使用一个新的数组存放最后的结果,用2个指针分别指向已知的两个数组头部,两个指针指向的值比较,较小的那个值存放到结果数组。

这里为了方便演示,num1不需要例子中的0占位,返回的结果也不需要必须是num1

public int[] merge(int[] num1, int m, int[] num2, int n) {
  int[] result = new int[m+n]; //用一个新的数组来存放最后的结果
  
  int p1 = 0; //指向num1数组的头
  int p2 = 0; //指向num2数组的头
  int index = 0; //指向result数组的头
  
  while(p1 < m && p2 < n) {
    result[index++] = num1[p1] < num2[p2] ? num1[p1++] : num2[p2++];
  }
  
  //上述while循环跳出,说明肯定有一个数组遍历完了。下面两个if只有一个能执行。
  if (p1 < m) {//num1数组还有没遍历到的元素
    // 把num1数组从p1开始拷贝,拷贝到result数组(从index开始,拷贝m+n-index个元素)
    System.arraycopy(num1, p1, reuslt, index, m+n-index);
  }
  if (p2 < n) {//num2数组还有没遍历到的元素
    System.arraycopy(num2, p2, reuslt, index, m+n-index);
  }
  
  return result;
}

解法3:尾部双指针

num1={1,3,5,7,9,0,0,0,0}

num2={2,4,6,8}

优化空间复杂度,不使用额外的数组。

用两个指针分别指向以上数组尾部(有效位),再弄一个指针指向num1最后(m+n),谁大就放在num1的最后。

public int[] merge(int[] num1, int m, int[] num2, int n) {
  int p1 = m-1; //指向num1数组的有效位的末尾
  int p2 = n-1; //指向num2数组的末尾
  int index = m+n-1; //指向num1的真末尾
  
  while(p1 >= 0 && p2 >= 0) {
    result[index--] = num1[p1] > num2[p2] ? num1[p1--] : num2[p2--];
  }
  
  // 如果num1数组的0号位的数比num2的0号位数小,那么num2肯定会移动完,此时正好满足num1就是最后结果;
  // 相反时,num2肯定有没移动完的数,此时需要拷贝。拷贝数的个数是p2+1(如果是上一种情况,p2=-1,也满足)
  System.arraycopy(num2, 0, num1, 0, p2+1);
  
  return num1;
}

11.子数组最大平均数

给一个整数数组,找出平均数最大且长度为k的下标连续的子数组,并输出该最大平均数。
输入:[1,12,-5,-6,50,3], K=4
输出:12.75
最大平均数(12-5-6+50)/4=51/4=12.75

解法:滑动窗口(固定差的双指针)

长度为k的区间窗口,在数组中移动,找出最大和的窗口。

先计算0-k区间的总和sum,然后移动后面那个指针,每移动一次得到的和sum等于上一次sum减去上一窗口的起始值再加上此次窗口的末尾值。

public double findMax(int[] nums, int k) {
  // 求一个窗口的和
  int sum = 0;
  for (int i = 0; i < k; i++) {
    sum += nums[i];
  }
  
  // 求所有窗口之中最大的和
  int max = sum;
  for (int i = k; i < nums.length; i++) {
    sum = sum - nums[i-k] + nums[i];
    max = Math.max(sum, max);
  }
  
  return 1.0 * max / k;
}

12.二叉树的最小深度

给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

解法1:递归

深度优先

根节点(其实是每个节点)的最小深度等于左子树的最小深度 和 有子树的最小深度 两者之间的最小值,然后递归,递归结束标志是没有子节点(即到了叶子节点)。

public int minDepth(TreeNode root) {
  if (root == null) {
    return 0;
  }
  
  // 其实,就是叶子节点时,深度是1;
  if (root.left == null && root.right == null) {
    return 1;
  }
  
  // 如果不是叶子节点,那么该节点的深度等于左右子树的最小深度中较小的那个,递归。
  int min = 0;
  if (root.left != null) {
    min = Math.min(minDepth(root.left), min);
  }
  if (root.right != null) {
    min = Math.min(minDepth(root.right), min);
  }
  
  // 那么该节点的最小深度就等于 子树的最小节点 ➕ 1
  return min + 1;
}

解法2:广度优先

从根节点开始,对于每遍历一个节点都放在队列中。然后对非空的队列进行取节点,根节点计最小深度为1,如果存在叶子节点,最小深度加1。。。

13.最长连续递增序列

给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。

解法:贪心算法

遍历数组,每次标记好起始位,然后往后遍历,到遍历到不满足要求的元素,计算最大长度,并重置其实位,依次往后,每次都是得到最大的值。

[1,2,3, 2,3,4, 3,4,5,6,7]

public int maxList(int[] nums) {
  int start = 0;
  int max = 0;
  for (int i = 1; i < nums.length; i++) {
    if (nums[i] < nums[i-1]) {
      max = Math.max(i - start, max);
      start = i;
    }
  }
}

14.柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为5美元。顾客排队购买你的产品,一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付5美元、10美元或20美元。必须给每个顾客正确找零
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回true,否则返回false.

解法:贪心算法

只有3中币种:5,10,20

对于5元,直接收下;

对于10元,只有一种找零,找5元;

对于20元,有两种找零方式,10+5,或 5+5+5;

但10+5找零是最优解,因为5元是万能的。

public boolean change(int[] bills) {
  //初始状态,卖家手里没有钱
  //申明手里三种币种的张树,20元的不用管,因为20元收到后对找零这件事没帮助。
  int five = 0;
  int ten = 0;
  
  for (int bill : bills) {
    if (bill == 5) {//直接收下
      five++;
    } else if (bill == 10) {
      if (five == 0) {//手里没有5元的
        return false;
      }
      //否则 
      five--;
      ten++;
    } else { //20
      if(five > 0 && ten > 0) {
        five--;
        ten--;
      } else if (five >= 3) {
        five -= 3;
      } else {
        return false;
      }
    }
  }
  
  return true;
}

15.三角形的最大周长

给定由一些正数(代表长度)组成的数组arr,返回由其中三个长度组成的、面积不为零的三角形的最大周长。
如果不能形成任何面积不为零的三角形,返回0

解法:贪心算法

对数组排序,然后取最大的3个数,判断这个数中较小的两个数之和大于最大的那个数,那么就满足要求。

public int maxLength(int[] nums) {
  Arrays.sort(nums);
  for (int i = nums.length-1; i >= 2; i++) {
    if(nums[i] < nums[i-1] + nums[i-2]) {
      return nums[i] + nums[i-1] + nums[i-2];
    }
  }
  return 0;
}

16.二叉树遍历

解法1:递归

//前序遍历
void preOrder(TreeNode root){
  if(root == null){
    return;
  }
  System.out.println(root.val);
  preOrder(root.left);
  preOrder(root.right);
}

//中序遍历
void inOrder(TreeNode root){
  if(root == null){
    return;
  }
  preOrder(root.left);
  System.out.println(root.val);
  preOrder(root.right);
}


//后序遍历
void postOrder(TreeNode root){
  if(root == null){
    return;
  }
  preOrder(root.left);
  preOrder(root.right);
  System.out.println(root.val);
}

解法2:迭代

//前序遍历
void preOrder(TreeNode root){
  if(root == null){
    return;
  }
  
  Stack<TreeNode> stack = new Stack<TreeNode>();
  stack.push(root);
  
  while(!stack.empty()){
    TreeNode node = stack.pop();
    System.out.println(node.val);
    if(node.right!=null){
      stack.push(node.right);
    }
    if(node.left != null){
      stack.push(node.left);
    }
  }
}


//中序遍历
void inOrder(TreeNode root){
  Stack<TreeNode> stack = new Stack<TreeNode>();
  TreeNode current = root;
  while(current != null|| !stack.empty()){
    while(current != null){//把所有左树都放进stack里
      stack.push(current);
      current = current.left;
    }
    current = stack.pop();
    System.out.println(current.val);
    current = current.right;
  }
}


//后序遍历

17.字符串查找

问题:字符串str1和str2,str1是否包含str2,如果包含返回str2在str1中开始的位置,不存在返回-1
例如:str1:“ABC12345de”,str2:"12345" 就是一个连续子串

解法1:暴力算法

// force find
int getIndexSubString(const string s1,const string s2){
    // 特殊情况
    if(s1.length()<1 || s2.length()<1 || s1.length()<s2.length()) return -1;
    
    // 正常情况:s1.length >=s2.length
    int j=0;
    for(int i=0;i<s1.length();i++){
        for(j=0;j<s2.length();j++){
            if(s1[i+j]!=s2[j]) break; // 只要有一个不相等就break
        }
        //没有break说明存在或者遍历完还没有
        if(j>s2.length()-1){
            return i;
        }
    }
    return -1;
}

解法2:kwp算法

18.打家劫舍

一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋都被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

输入:[1,2,3,1] 输出:4

输入:[2,7,9.3,1] 输出:12

解法1:递归

第index位的数组的最优解等于,第index-1和第index-2最优解加上nums[index]两者中最大的值。

public int maxMoney(int[] nums, int index) {
  if(nums == null || index < 0) {
    return 0;
  }
  if (index = 0) {
    return nums[0];
  }
  return Math.max(maxMoney(nums, index-1), maxMoney(index-2) + nums[index]);
}

解法2:动态规划

以上递归解法,会出现重复计算某些index情况。优化:可以用一个数组来存放所有index情况下的最优值,动态迭代

public int maxMoney(int[] nums) {
  int length = nums.length;
  if(nums == null || length == 0) {
    return 0;
  }
  if (index = 1) {
    return nums[0];
  }
  
  //迭代来存放所有index情况下的最优解
  int[] dp = new int[nums.length];
  dp[0] = nums[0];
  dp[1] = Math.max(nums[0], nums[1]);
  for (int index = 2; index < nums.length; index++) {
    dp[index] = Math.max(dp[index-1], dp[index-2] + nums[index]);
  }
  return dp[length-1];
}

动态规划:最优子结构----递推----重叠子问题

上一篇下一篇

猜你喜欢

热点阅读