剑指Offer算法题笔记(第三期,汇总版)

本文精选了30道经典算法题目,覆盖了从二维数组查找、链表操作到二叉树遍历、数据流处理等核心内容,每道题目均附有详细解析与AC代码,旨在帮助读者深入理解算法原理与实践技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

剑指Offer算法题笔记

1. 二维数组的查找

题目

  • 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路:

  • 对每一行进行二分搜索。
    注意点

  • 二分搜索是通过把start和end复制为mid+1和mid-1来实现的。循环条件为start<=end。

AC代码

public class Solution {
    public boolean Find(int target, int [][] array) {
        for(int i=0;i<array.length;i++){
            int start = 0;
            int end = array[i].length-1;
            while(start<=end){
                int mid = (start+end)/2;
                if(array[i][mid]<target){
                    start = mid+1;
                }else if(array[i][mid]>target){
                    end = mid-1;
                }
                else
                    return true;
            }
        }   
        return false;
    }
}

2. 替换空格

题目

  • 请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

思路:

  • 从前向后计数空格个数,从后向前移位。

为什么?

  • 如果从前向后移位,假设为we are one.那么遇到第一个空格的时候,are就要向后移位,one也要向后移位。然后遇到第二个空格的时候,one还要向后移位。但是从后向前只需要移动一次,注意点是提前把位置留好就行。

AC代码

//思路,先从前到后遍历找到所有的空格,然后从后向前移位。
//对于不是空格的,直接按位置分配。对于是空格的,按位置分配%20,并且spaceCnt--。
public class Solution {
    public String replaceSpace(StringBuffer str) {
        int spaceCnt = 0;
        int length = str.length();
        for(int i=0;i<length;i++){
            if(str.charAt(i)==' ')
                spaceCnt++;
        }
        int newLength = spaceCnt*2+length;
        str.setLength(newLength);
        for(int i=length-1;i>=0;i--){
            if(str.charAt(i)!=' '){
                str.setCharAt(i+spaceCnt*2,str.charAt(i));
            }else{
                spaceCnt--;
                str.replace(i+2*spaceCnt,i+2*spaceCnt+3,"%20");    
            }
        }
        return str.toString();
    }
}

3. 用两个栈实现一个队列

思路:

  • stack1用于入队列,stack2用于出队列。当仅当stack2为空的时候,stack1将所有元素全部放置入stack2中

AC代码

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if(stack2.isEmpty()){
            while(!stack1.isEmpty()){
                Integer i = stack1.pop();
                stack2.push(i);
            } 
        }

        return stack2.pop();
    }
}

4. 旋转数组的最小数字

题目:

  • 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

思路

  • 思路1 顺序遍历
  • 思路2 二分查找

AC代码

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] array) {
        int right = array.length-1;
        int left = 0;
        int mid = 0;
        if(right == -1)
            return 0;
        while(array[left] >= array[right]){
            if(right-left==1){
                mid = right;
                break;
            }
            mid = (right+left)/2;
            if(array[mid] >= array[left])
                left = mid;
            else if(array[mid] <= array[right])
                right = mid;
        }
        return array[mid];
    }
}

5. 整数的整数次方

题目:

  • 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

思路:

  • 难点完全在如何通过递归实现快速乘方

AC代码

public class Solution {
private double PowerWithRecursion(double base,int n){
    if(n==0)
        return 1;
    if(n==1)
        return base;
    double res = PowerWithRecursion(base,n>>1);
    res *= res;
    if((n&0x01)==1)
        res *= base;
    return res;

}
public double Power(double base, int exponent) {
    double res = 1;
    if(Math.abs(base-0)<0.0001&&exponent<0)
        return -1;
    res = PowerWithRecursion(base,Math.abs(exponent));
    return exponent>0?res:1/res;

}
}

6. 重建二叉树

题目

  • 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

思路

  • 本地重点还是要理解清楚二叉树的先序遍历和中序遍历。必须要有中序遍历才能确定唯一的一个二叉树,因为中序遍历给出了根节点是谁。
  • 代码解释:首先从先序遍历中获得根节点,然后根据根节点在中序遍历中的位置,找到index。这个index就是中序遍历数组中区分左右子树的分隔位置。如果你写过快排的程序,那你应该很快速的想到,下面就是分治法解决问题。

AC代码

import java.util.*;
/**
* Definition for binary tree
* public class TreeNode {
*     int val;
*     TreeNode left;
*     TreeNode right;
*     TreeNode(int x) { val = x; }
* }
*/
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if(pre.length==0||in.length==0)
            return null;
        //找到中序遍历中区分左右子树的分割位置,即根节点位置
        int index = 0;
        for(int i=0;i<in.length;i++){
            if(in[i] == pre[0]){
                index = i;
                break;
            }
        }
        
        //左子树,右子树,递归。
        TreeNode root = new TreeNode(pre[0]);
        root.left = reConstructBinaryTree(Arrays.copyOfRange(pre,1,index+1),Arrays.copyOfRange(in,0,index));
        root.right = reConstructBinaryTree(Arrays.copyOfRange(pre,index+1,pre.length),Arrays.copyOfRange(in,index+1,in.length));
        return root;
    }
}

7. 反转链表

题目

  • 输入一个链表,反转链表后,输出新链表的表头。

思路

  • 把preNode当做是null,将head.next先保存下来,然后将head指向preNode。然后依次前进一格,即head变成nextNode,preNode变成head。值得注意的是,head=null的时候说明已经全部反转完毕,但是头不是head,而是head还没有被复制为null的时候。

AC代码

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        if(head==null||head.next == null)
            return head;
        ListNode preNode = null;
        ListNode resNode = null;
        while(head!=null){
            ListNode nextNode = head.next;
            if(nextNode == null)
                resNode = head;
            head.next = preNode;
            preNode = head;
            head = nextNode;
        }
        return resNode;
    }
}

8. 合并两个排序的链表

题目

  • 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

AC代码

  • 递归版本,典型的分治法思路。

      public class Solution {
          public ListNode Merge(ListNode list1,ListNode list2) {
              if(list1==null)
                  return list2;
              if(list2==null)
                  return list1;
              ListNode head = null;
              if(list1.val<list2.val){
                  head = list1;
                  head.next = Merge(list1.next,list2);
              }
              if(list1.val>=list2.val){
                  head = list2;
                  head.next = Merge(list1,list2.next);
              }
              return head;
          }
      }
    
  • 非递归版本,本质上是新建一个头结点然后在头结点上根据元素大小增加节点。值得注意的是,当list1或者list2为null时,while循环会结束,这只能代表为null的那个链表已经完全被添加到新链表中,但是不为null的那个可能还没有添加进去。注意返回值,一开始就要备份头节点、

      public class Solution {
          public ListNode Merge(ListNode list1,ListNode list2) {
              if(list1==null)
                  return list2;
              if(list2==null)
                  return list1;
              ListNode mergeHead = new ListNode(-1);
              ListNode newHead = mergeHead;
              
              while(list1!=null&&list2!=null){
                  if(list1.val < list2.val){
                      mergeHead.next = list1;
                      list1 = list1.next;
                  }
                  else{
                      mergeHead.next = list2;
                      list2 = list2.next;
                  }
                  mergeHead = mergeHead.next;
              }
              if(list1!=null) mergeHead.next = list1;
              if(list2!=null) mergeHead.next = list2;
              return newHead.next;
    
          }
      }
    

9. 树的子结构

题目

  • 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

思路

  • HasSubtree思路为:先找到root节点相同的,然后比较左右子树(这个过程通过helper完成)。如果root节点相同,但是左右子树比较出的结果是false,那么就需要向下继续遍历root1,看看是否有根节点相同的。helper的思路是,如果root2被遍历完,说明是子结构。如果root2还没遍历完,但是root1遍历完了,那说明root1太短了,因此false,如果root1和root2中有数据不一样,那么仍然是false。返回的结果是左右子树的&&。

AC代码

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    private boolean helper(TreeNode root1,TreeNode root2){
        if(root2==null)
            return true;
        if(root1==null)
            return false;
        if(root1.val != root2.val)
            return false;
        
        return helper(root1.left,root2.left)&&helper(root1.right,root2.right);
    }
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        boolean result = false;
        if(root1!=null&&root2!=null){
            if(root1.val==root2.val){
                result = helper(root1,root2);
            }
            if(!result){
                result = HasSubtree(root1.left,root2);
            }
            if(!result){
                result = HasSubtree(root1.right,root2);
            }
        }
        return result;

    }
}

10. 包含min函数的栈

题目

  • 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

思路

  • 辅助栈的大小始终和原栈保持一致。min方法实际是查看helper栈顶元素。

    • 原栈 5(最后入栈) 3 3 8 9 8(最先入栈)
    • 辅助 3(最后入栈) 3 3 8 8 8(最先入栈)

    原栈弹出5,辅助弹出3,原栈弹出3,辅助弹出3.
    此时

    • 原栈 3 8 9 8
    • 辅助栈 3 8 8 8

    发现辅助栈中栈头为3,确实是最小的。

AC代码

import java.util.Stack;

public class Solution {
    Stack<Integer> stack = new Stack();
    Stack<Integer> helpStack = new Stack();
    int min = Integer.MAX_VALUE;
    public void push(int node) {
        stack.push(node);
        if(node <= min){
            helpStack.push(node);
            min = node;
        }    
        else
            helpStack.push(min);
            
    }
    
    public void pop() {
        stack.pop();
        helpStack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int min() {
        return helpStack.peek();
    }
}

11. 栈的压入、弹出序列

题目:

  • 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

思路:

  • 采用一个辅助栈的方式。如果辅助栈顶和弹出栈值相同,则一起弹出,否则辅助栈一直入栈。两个序列按此操作,全部填满后一起弹出,如果弹出时有不同元素则false,一起弹出至空,为true。

AC代码

import java.util.ArrayList;
import java.util.Stack;


public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        Stack<Integer> helpStack = new Stack();
        for(int i=0;i<popA.length;i++){
            helpStack.push(popA[i]);
        }
        Stack<Integer> stack = new Stack();
        for(int i=0;i<pushA.length;i++){
            stack.push(pushA[i]);
            if(helpStack.peek()==stack.peek()){
                helpStack.pop();stack.pop();
            }
        }
        while(!helpStack.isEmpty()||!stack.isEmpty()){
            if(helpStack.peek()!=stack.peek()){
                return false;
            }else{
                helpStack.pop();stack.pop();
            }
        }
        return true;

    }
}

12. 二叉搜索树的后序遍历序列

题目

  • 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

思路

  • 以数组末尾为root切分,比root小的必然是root的左子树,比root大的必然是root的右子树。如果切分后,发现双指针没有相遇,说明不符合BST的要求,必然为false。值得注意的是,java中不能写成while(i<=end&&array[i++]<root);会有问题。要写成while(i<=end&&array[i]<root) i++;还有要注意的,要小心end前面的数组是单调递增或者递减,在while中要判断越界。

AC代码

public class Solution {
    private boolean helper(int [] array,int start,int end){
        if(start >= end)
            return true;
        int root = array[end];
        int i = start;
        int j = end-1;
        while(i<=end&&array[i]<root) i++;
        i--;
        while(j>=0&&array[j]>root) j--;
        j++;
        if(j-i!=1)
            return false;
        return helper(array,start,i) && helper(array,j,end-1);     
    }
    
    public boolean VerifySquenceOfBST(int [] sequence) {
        int end = sequence.length-1;
        if(end==-1)
            return false;
        return helper(sequence,0,end);
    }
}

13. 二叉树中和为某一值的路径

题目

  • 输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

思路

  • 类似前序遍历的去查一棵树。为什么要前序遍历呢?因为只有前序遍历是率先访问根节点。递归汇总为什么访问完左右子树之后要remove呢?是因为树的递归回去的时候,path中应该去掉当前节点的值。

AC代码

import java.util.ArrayList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    private void helper(TreeNode root,int target,ArrayList<Integer> path,ArrayList<ArrayList<Integer>> res){
        if(root==null)
            return;
        path.add(root.val);
        if(target==root.val&&root.left==null&&root.right==null){
            res.add(new ArrayList(path));
        }else{
            helper(root.left,target-root.val,path,res);
            helper(root.right,target-root.val,path,res);
        }
        path.remove(path.size()-1);
        
    }
    
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        ArrayList<Integer> path = new ArrayList();
        ArrayList<ArrayList<Integer>> res = new ArrayList();
        helper(root,target,path,res);
        return res;
    }
}

14. 复杂链表的幅值

题目

  • 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

思路

  • 如图

AC代码

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    private void cloneNode(RandomListNode pHead){
        RandomListNode node = pHead;
        while(node!=null){
            RandomListNode newNode = new RandomListNode(node.label);
            newNode.random = null;
            newNode.next = node.next;
            node.next = newNode;
            node = newNode.next;
        }
    }

    private void connectRandomNode(RandomListNode pHead){
        RandomListNode node = pHead;
        while(node!=null){
            if(node.random!=null){
                node.next.random = node.random.next;
            }
            node = node.next.next;
        }
    }
    
    private RandomListNode recombineNode(RandomListNode pHead){
        RandomListNode newHead = pHead.next;
        RandomListNode node = pHead;
        while(node!=null){
            RandomListNode curNode = node.next;
            node.next = curNode.next;
            if(curNode.next!=null)
                curNode.next = curNode.next.next;
            node = node.next;
        }
        return newHead;
    }
    
    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead==null)
            return null;
        cloneNode(pHead);
        connectRandomNode(pHead);
        return recombineNode(pHead);
    }
}

15. 二叉搜索树与双向链表

题目

  • 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

思路

  • 遍历方向 右 中 左。建立一个全局变量list,每次中序遍历出来的root要插入list中。怎么插入呢?如果list为空,那么当前root直接插入list。如果list不为空,那么此时弹出的值必然要比list小,因为右、中、左遍历,一定是大、中、小。因此将此时的root放置在list的左边即可。注意list永远表示的是双向链表中当前最小的那个值,因此list要赋值为root。

AC代码

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    TreeNode list = null;
    public TreeNode Convert(TreeNode pRootOfTree) {
        if(pRootOfTree == null)
            return null;
        Convert(pRootOfTree.right);
        if(list==null){
            list=pRootOfTree;
        }else{
            list.left=pRootOfTree;
            pRootOfTree.right=list;
            list = pRootOfTree;
        }
        Convert(pRootOfTree.left);
        return list;
    }
}

16. 字符串的排列

题目

  • 输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

思路

  • 分治法。ABC的全排列。首先确认第一个字母,有3种。假设是A,则转化成问题——BC的全排列。循环+swap可以保证第i个位置有length-i种排列方法。以第一空为例,A和A交换,A和B交换,B和C交换,第一个位置分别是A,B,C。序列分别为,ABC,BAC,CBA。仅看第一个位置,确实是3种可能全部走完。在A开头的情况下,递归处理BC的情况。则有BC,CB两种可能。这里也引出了递归的结束条件,当数据内元素到末尾时,因为只能和后面的交换,因此此时为截止、

AC代码

import java.util.*;
public class Solution {
    
    private void swap(char[] cs,int i,int j){
        char temp = cs[i];
        cs[i] = cs[j];
        cs[j] = temp;
    }
    
    private void PermutationHelper(char[] chars,int i,ArrayList<String> res){
        if(i==chars.length-1){
            res.add(String.valueOf(chars));
            return;
        }
        Set<Character> charSet = new HashSet();
        for(int j=i;j<chars.length;j++){
            if(j==i||!charSet.contains(chars[j])){
                charSet.add(chars[j]);
                swap(chars,i,j);
                PermutationHelper(chars,i+1,res);
                swap(chars,i,j);
            }
        }
    }
    
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> res = new ArrayList();
        if(str!=null && str.length()>0){
            PermutationHelper(str.toCharArray(),0,res);
            Collections.sort(res);
        }    
        return res;
    }
}

17. 最小的K个数

题目

  • 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

思路

  • 采用大顶堆,即根元素是最大的,子元素一定比根元素小。堆的大小为k,如果新数比堆顶大就舍弃,比堆顶小就插入。Java的PriorityQueue的底层实现就是小顶堆。

优先队列PriorityQueue特点

  • 每次取出都是最小值

AC代码

import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Solution {
    class MaxHeapComparetor implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2.compareTo(o1);
        }
    }
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        PriorityQueue<Integer> pq = new PriorityQueue<>(new MaxHeapComparetor());
        ArrayList<Integer> res = new ArrayList<>();
        if(input.length<k||k==0)
            return res;
        pq.offer(input[0]);
        for(int i=1;i<input.length;i++){
            if(pq.size()<k){
                pq.offer(input[i]);
            }else{
                if(pq.peek()>input[i]){
                    pq.poll();
                    pq.offer(input[i]);
                }
            }
        }
        int length = pq.size();
        for(int i=0;i<length;i++){
            res.add(pq.poll());
        }
        return res;
    }
}

18. 整数中1出现的数

题目

  • 求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

思路

  • 设N = abcde ,其中abcde分别为十进制中各位上的数字。
    如果要计算百位上1出现的次数,它要受到3方面的影响:百位上的数字,百位以下(低位)的数字,百位以上(高位)的数字。
    ① 如果百位上数字为0,百位上可能出现1的次数由更高位决定。比如:12013,则可以知道百位出现1的情况可能是:100199,11001199,21002199,,…,1110011199,一共1200个。可以看出是由更高位数字(12)决定,并且等于更高位数字(12)乘以 当前位数(100)。
    ② 如果百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响。比如:12113,则可以知道百位受高位影响出现的情况是:100199,11001199,21002199,,…,1110011199,一共1200个。和上面情况一样,并且等于更高位数字(12)乘以 当前位数(100)。但同时它还受低位影响,百位出现1的情况是:12100~12113,一共14个,等于低位数字(13)+1。
    ③ 如果百位上数字大于1(29),则百位上出现1的情况仅由更高位决定,比如12213,则百位出现1的情况是:100199,11001199,21002199,…,1110011199,1210012199,一共有1300个,并且等于更高位数字+1(12+1)乘以当前位数(100)。

AC代码

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int cnt = 0;
        int i =1;
        while(n/i!=0){
            int before = n/i/10;
            int current = (n/i)%10;
            int after = n%i;
            if(current==0){
                cnt += before*i;
            }else if(current==1){
                cnt += before*i+after+1;
            }else{
                cnt += before*i+i;
            }
            
            i*=10;
        }
        return cnt;
            
    }
}

19. 丑数

题目

  • 把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

思路

  • 动态规划思路。dp[i]代表的是第i个丑数是谁。dp[i] = Min{dp[i2]*2,dp[i3]*3,dp[i5]*5}。但是i2,i3,i5怎么确定呢?比如说dp[0]=1,那么显然i2\3\5都是0,dp[1]=2,此时i2就要后移一位,即自加一次。

    为什么要i2要自加呢?因为i2之前的坐标所代表的的值*2一定小于dp[i2]*2,那也一定小于当前最大的丑数M,因为dp[i2]2是数组中所有丑数2后大于M的最小的数。因此不需要再考虑i2坐标之前的数了,反正必然是小于M的。

    其实本来的逻辑应该是,当前丑数数列中的每一个都要分别乘2,3,5,然后找到大于M的最小值,但是因为如果dpi2*2是当前的最大丑数,那dp(i2-1)*2必然比M小,所以之后就不需要再考虑i2之前的了。。

AC代码:

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index<7)
            return index;
        int[] dp = new int[index];
        int i2=0,i3=0,i5=0;
        dp[0] = 1;
        for(int i=1;i<index;i++){
            int m2 = 2*dp[i2];
            int m3 = 3*dp[i3];
            int m5 = 5*dp[i5];
            dp[i] = Math.min(m2,Math.min(m3,m5));
            if(m2 == dp[i]) i2++;
            if(m3 == dp[i]) i3++;
            if(m5 == dp[i]) i5++;
        }
        return dp[index-1];
    }
}

20. 两个链表的第一个公共节点

题目

  • 输入两个链表,找出它们的第一个公共结点。

思路

  • 从后向前遍历,找到最后一个相同的。但是链表是单向的啊?没关系,我们可以用两个栈进行保存。

AC代码

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
import java.util.*;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        Stack<ListNode> stack1 = new Stack();
        Stack<ListNode> stack2 = new Stack();
        while(pHead1!=null){
            stack1.push(pHead1);
            pHead1 = pHead1.next;
        }
        while(pHead2!=null){
            stack2.push(pHead2);
            pHead2 = pHead2.next;
        }
        ListNode res = null;
        while(!stack1.isEmpty()&&!stack2.isEmpty()){
            if(stack1.peek()==stack2.peek()){
                res = stack1.pop();
                stack2.pop();
            }else{
                break;
            }
        }
        return res;
    }
}

21. 平衡二叉树

题目

  • 输入一棵二叉树,判断该二叉树是否是平衡二叉树。

思路

  • 判断二叉树深度,如果左右两边深度差距超过1,则直接向上返回,不再向下递归。

AC代码
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
return TreeDepth(root)!=-1;
}
public int TreeDepth(TreeNode root) {
if(rootnull)
return 0;
int left = TreeDepth(root.left);
if(left
-1)
return -1;
int right = TreeDepth(root.right);
if(right==-1)
return -1;
if(Math.abs(left-right)>1)
return -1;
else
return Math.max(left+1,right+1);
}
}

22. 数组中的逆序对

题目

  • 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

思路

  • 统计逆序对。[5,7 | 4,6]这个数列中,start=0,end=length-1,mid就是前半部分的结束为止。5比4大,说明有一个逆序对?不,因为5后面的数字必然比5大,自然也必然比4大,因此从5到mid+1下标都是逆序对,因此cnt += mid+1-i。
  • 归并排序。归并排序用到了典型的分治法思想,把数组分成两份,再分成两份,直到只有一个数。因为一个数必然有序,然后将两个数合成一个2元素的数组,合并的过程用merge函数完成,然后将两个两个元素的数组合成4个元素的有序数组,最后合成一个完全有序的数组。

AC代码

public class Solution {
    public int cnt = 0;
    public void mergeSort(int[] a,int start,int end){
        if(start>=end)
            return;
        int mid = (start+end)/2;
        mergeSort(a,start,mid);
        mergeSort(a,mid+1,end);
        merge(a,start,mid,end);
    }

    public void merge(int[] a,int start,int mid,int end){
        int[] tmp = new int[end-start+1];
        int i=start;
        int j=mid+1;
        int k=0;
        while(i<=mid&&j<=end){
            if(a[i]<=a[j]){
                tmp[k++]=a[i++];
            }
            else if(a[i]>a[j]){
                cnt += mid-i+1;
                cnt = cnt%1000000007;
                tmp[k++]=a[j++];
            }
        }
        while(i<=mid)
            tmp[k++]=a[i++];
        while(j<=end)
            tmp[k++]=a[j++];
        for(i=0;i<tmp.length;i++){
            a[start+i] = tmp[i];
        }
    }
    public int InversePairs(int [] array) {
        cnt=0;
        mergeSort(array,0,array.length-1);
        return cnt;
    }
}

23. 圆圈中最后剩下的数

题目

  • 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

思路

  • 每次删除之后,要记住后面的id都向前移动了一位。第一次remove的是id=0+m-1。在index都不变的情况下,第二次删除的编号应该为id += m-1 + 1,但是因为删除掉了id,所以id+=m-1.

AC代码

import java.util.LinkedList;
public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        LinkedList<Integer> list = new LinkedList<>();
        if(n==0||n==1)
            return n-1;
        for(int i=0;i<n;i++)
            list.add(i);
        int id=0;
        while(list.size()>1){
            id = (id+m-1)%list.size();
            list.remove(id);
        }
        return list.getFirst();
    }
}

24. 求1+2+3+…+n

题目

  • 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

思路

  • 基本算法是用递归来写的,但是要有边界条件,即当n0的时候返回。但是现在的问题是不能用if语句,因此用ans!=0&&来完成if语句的功能。即,当ans0时,由于&&的短路特性,不会执行&&之后的语句,以此作为边界的判断条件。

AC代码

public class Solution {

    public int Sum_Solution(int n) {
        int ans = n;
        boolean flag = (ans!=0) && ((ans+=Sum_Solution(n-1))!=0);
        return ans;
    }
}

25. 不用加减乘除做加法

题目

  • 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

思路

  • 两数字异或可以得到没有进位的加法结果,因为1+1=0,11=0。0+0=0,00=0。1+0=1,1^0=1。两数字相与可以得到数字的某一位上是1,当且仅当这两个数的同一位为1时,才会产生进位。进位应该是向前进一位,因此要(a&&b)<<1

AC代码

public class Solution {
    public int Add(int num1,int num2) {
        while(num2!=0){
            int a = num1 ^ num2;
            int b = (num1 & num2) << 1;
            num1 = a;
            num2 = b;
        }
        return num1;
    }
}

26. 链表中环的入口节点

题目

  • 给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

思路

  • 快慢指针判断是否有环。若slow==fast,将fast放到最前,与slow同速度,当他们相遇时,就是环入口。

    AC代码

      public class Solution {
      public ListNode EntryNodeOfLoop(ListNode pHead)
      {
          if(pHead==null||pHead.next==null||pHead.next.next==null)
              return null;
          ListNode slow = pHead.next;
          ListNode fast = pHead.next.next;
          while(slow!=fast){
              if(slow!=null&&fast!=null&&fast.next!=null){
                  slow = slow.next;
                  fast = fast.next.next;
              }else
                  return null;
          }
          fast = pHead;
          while(fast!=slow){
              fast = fast.next;
              slow = slow.next;
          }
          return slow;
      }
    

    }

27. 序列化二叉树

题目

  • 请实现两个函数,分别用来序列化和反序列化二叉树

思路

  • 序列就是将二叉树进行前序遍历,然后null用#表示,每个节点之间用“,”分割,这是为了防止出现多位数,比如112,到底是1,1,2,还是11,2,还是112?用逗号分割后就不存在这个问题了。从字符串恢复树的过程就是建树过程,注意的点是运用了一个index进行递归。index表示目前是以index节点为root节点的树。

AC代码

    public class Solution {
        int index = -1;
        String Serialize(TreeNode root) {
            StringBuffer sb = new StringBuffer();
            if(root==null){
                sb.append("#,");
                return sb.toString();
            }
            sb.append(root.val+",");
            sb.append(Serialize(root.left));
            sb.append(Serialize(root.right));
            return sb.toString();

    }
        TreeNode Deserialize(String str) {
            index++;
            String[] strs = str.split(",");
            if(!strs[index].equals("#")){
                TreeNode root = new TreeNode(Integer.valueOf(strs[index]));
                root.left = Deserialize(str);
                root.right = Deserialize(str);
                return root;
            }
            return null;
            
    }
    }

28. 二叉搜索树的第K个节点

题目

  • 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

思路

  • 二叉搜索树的中序遍历就是一个递增序列。在做中序遍历时,当计数器等于k时则返回相应节点。这里要做剪枝的处理,即提前返回。每次递归之后要判断是否满足剪枝条件,如果满足,就提前返回,不再继续递归。

AC代码

    public class Solution {
        int cnt = 0;
        TreeNode KthNode(TreeNode pRoot, int k)
        {
            if(pRoot==null)
                return null;
            TreeNode node = KthNode(pRoot.left,k);
            if(node!=null)
                return node;
            cnt++;
            if(cnt==k)
                return pRoot;
            node = KthNode(pRoot.right,k);
            if(node!=null)
                return node;
            return null;
        }
        
    }

29. 数据流中的中位数

题目

  • 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

思路

  • 想要得到中间的数字不一定非要排序。可以用两个容器,左边放较小的,右边放较大的,保证左边的容器中的所有数据都比右边容器中的最小值还要小。
  • 下面考虑用什么容器?考虑用堆,因为这样获取最小值和最大值的时间复杂度为O(1),插入的时间复杂度为O(n)。
  • 具体而言,用最大堆放较小的数,用最小堆放较大的数。以此保证最大堆中可以常数时间获取较小数中最大的数,从最小堆中以常数时间获得最大数中最小的数。
  • 但是,为了保证两堆中数量差不超过1,因此可以奇,偶插入不同的堆。但是插入最小堆之前要先插入最大堆,然后取出最大堆中最大的,再插入最小堆,这样保证最大堆(较小值容器)中取出的一定是最大值,再把最大值放入最小堆(较大值容器)。反之亦然,插入最大堆之前要插入最小堆。

AC代码

import java.util.*;
public class Solution {
    int cnt = 0;
    private PriorityQueue<Integer> minHeap = new PriorityQueue();
    private PriorityQueue<Integer> maxHeap = new PriorityQueue(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2.compareTo(o1);
        }
    });
    public void Insert(Integer num) {
        if(cnt%2==0){
            maxHeap.offer(num);
            int maxNum = maxHeap.poll();
            minHeap.offer(maxNum);
        }else{
            minHeap.offer(num);
            int minNum = minHeap.poll();
            maxHeap.offer(minNum);
        }
        cnt++;
    }

    public Double GetMedian() {
        int depth = maxHeap.size()-minHeap.size();
        if(depth==0){
            return (maxHeap.peek()+minHeap.peek())/2.0;
        }else{
            return minHeap.peek()/1.0;
        }
    }

}

30. 滑动窗口的最大值

题目

  • 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

思路

  • 思路1:用数组实现,通过标记index和max(index指的是max的下标)。如果index还在窗口内,就只比较当前最大值和新进窗口的值。如果index已经不在窗口内了,那就重新遍历窗口,得到一个最大值和最大值的下标。
  • 思路2:用双端队列实现。双端队列中保存下标,如果新入窗口的下标-最大值下标已经超过了size,就说明原最大值下标应该被poll出去。队列中保存的是什么呢?队列头保存的是当前的最大值下标,队列尾保存的是比队列头小的数,而且是仅次于队列头的数。所以双端队列里永远只有两个值。一个是最大值的下标,一个是次大值的下标。

AC代码

  1. 改进后的最笨办法

     import java.util.*;
     public class Solution {
         public ArrayList<Integer> maxInWindows(int [] num, int size)
         {
             ArrayList<Integer> res = new ArrayList();
             if(num.length<size||size==0)
                 return res;
             int index = 0;
             int max = num[0];
             for(int i=0;i<num.length;i++){
                 if(i-index>=size){
                 max = num[index+1];
                 for(int j=index+1;j<=i;j++){
                     if(num[j]>=max){
                         max = num[j];
                         index=j;
                     }
                 } 
                 }
                 else if(num[i]>=max){
                     index=i;
                     max=num[i];
                 }
                 if(i>=size-1){
                     res.add(max);
                 }
             }
             return res;
         }
     }
    
  2. 双端队列

     import java.util.*;
     public class Solution {
         public ArrayList<Integer> maxInWindows(int [] num, int size)
         {
             ArrayList<Integer> res = new ArrayList<>();
             ArrayDeque<Integer> deque = new ArrayDeque<>();
             if(size==0||num.length==0)
                 return res;
             for(int i=0;i<num.length;i++){
                 if(!deque.isEmpty()&&i-deque.peekFirst()>=size){
                     deque.pollFirst();
                 }
                 if(deque.isEmpty())
                     deque.offerFirst(i);
                 else if(num[i]>=num[deque.peekFirst()]){
                     deque.clear();
                     deque.offerFirst(i);
                 }else{
                     while(num[deque.peekLast()]<=num[i]){
                         deque.pollLast();
                     }
                     deque.offerLast(i);
                 }
                 if(i>=size-1)
                     res.add(num[deque.peekFirst()]);
             }
             return res;
         }
     }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值