热题100简单难度
1、合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
送分题,注意特殊情况即可。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
//t1 t2为空的情况
if(t1==null||t2==null)
return (t1==null)?t2:t1;
//t1 t2均不为空
TreeNode newT=new TreeNode(t1.val+t2.val);
newT.left=mergeTrees(t1.left,t2.left);
newT.right=mergeTrees(t1.right,t2.right);
return newT;
}
}
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func mergeTrees(t1 *TreeNode, t2 *TreeNode) *TreeNode {
if t1==nil{
return t2
}
if t2==nil{
return t1
}
t1.Val,t1.Left,t1.Right=
t1.Val+t2.Val,
mergeTrees(t1.Left,t2.Left),
mergeTrees(t1.Right,t2.Right)
return t1
}
2、汉明距离
给出两个整数 x 和 y,计算它们之间的汉明距离。
注意:
0 ≤ x, y < 231.
示例:
输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。
灵光一闪,想到了一种很别致的方法,试了几组数据确实没错,或运算和与运算之后的差值就是汉明距离,也就是二进制位不同的位数。
class Solution {
public int hammingDistance(int x, int y) {
//规律总结
return calTotalBits(x|y)-calTotalBits(x&y);
}
//计算二进制中为1的数目
public int calTotalBits(int x){
int count=0;
while(x!=0){
if((x&0x1)==1)
++count;
x>>=1;
}
return count;
}
}
3、翻转二叉树
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
送分题,注意特殊情况就行了。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if((root==null)||root.left==null&&root.right==null){
return root;
}
else{
TreeNode tempNode=root.left;
root.left=root.right;
root.right=tempNode;
}
invertTree(root.left);
invertTree(root.right);
return root;
}
}
4、二叉树的最大深度
定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
送分题,暴力递归,或者使用先进先出的队列也可以实现BFS。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root==null)
return 0;
else if(root.left==null && root.right==null)
return 1;
else{
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
}
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func maxDepth(root *TreeNode) int {
// 解法1:递归
// if root==nil{
// return 0
// }
// if root.Left==nil && root.Right==nil{
// return 1
// }
// return int(math.Max(float64(maxDepth(root.Left)),float64(maxDepth(root.Right))))+1
// 解法2:BFS 使用slice
if root==nil{
return 0
}
depth:=0
queue:=make([]*TreeNode,0)
queue=append(queue,root)
for len(queue)>0{
curLen:=len(queue)
depth++
for i:=0;i<curLen;i++{
// 先进先出 下一层入队
node:=queue[0]
queue=queue[1:]
if node.Left!=nil{
queue=append(queue,node.Left)
}
if node.Right!=nil{
queue=append(queue,node.Right)
}
}
}
return depth
}
5、反转链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
一般思路,申请ArrayList存储节点值,然后依次覆盖原节点,T(N)=O(N),S(N)=O(N)。
两个指针轮换,后一个指向前一个,知道后一个所指为空,反转结束。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null;
ListNode curr=head;
ListNode nex=null;
//遍历未结束
while(curr!=null){
//记录下一节点
nex=curr.next;
//当前节点指向前一节点
curr.next=pre;
//节点更新
pre=curr;
curr=nex;
}
return pre;
}
}
6、只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
HashTable,快速排序,暴力循环,还有一种取巧的方法是用数学 2*(a+b+c)-(a+b+b+c+c)=a。
class Solution {
public int singleNumber(int[] nums) {
//对所有的数字做异或,剩下的就只出现一次
//T(N)=O(N),S(N)=O(1)
for(int i=1;i<nums.length;++i){
nums[0]^=nums[i];
}
return nums[0];
}
}
7、多数元素
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [3,2,3]
输出: 3
示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
方法一,暴力解法,判断每个数字时遍历nums,确定是否为众数元素,T(N)=O(N),S(N)=O(1)。
方法二,使用HashMap<Integer,Integer>遍历一次来存储nums所有元素及其出现次数,然后再根据HashMap.keySet()进行遍历,找出众数元素,T(N)=O(N),S(N)=O(N)。
class Solution {
public int majorityElement(int[] nums) {
//以Hashmap存储每个数字(key)以及出现的次数(value)
Map<Integer,Integer> map=new HashMap<>();
int finalResult=-1;
for(int i=0;i<nums.length;++i){
if(map.containsKey(nums[i]))
map.put(nums[i],map.get(nums[i])+1);
else
map.put(nums[i],1);
}
//按照HashMap的keySet()遍历所有key,选出多数元素
for(Integer i:map.keySet()){
if(map.get(i)>(nums.length/2)){
finalResult=i;
break;
}
}
return finalResult;
}
}
方法三,众数元素出现次数超过n/2,则按序排列后必然位于中间的位置,总数为奇数则位于(n-1)/2,总数为偶数则位于n/2处(0为起始),反正都是nums[nums.length/2],排序最快为快速排序或者Arrays.sort(),T(N)=O(nlgn),若数组恰好逆序,则S(N)=O(N),否则S(N)=O(1)。
8、合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
方法一,递归方法,首先判断是否非空,之后,将小者作为头结点,剩下两个链表进行合并(此过程中原链表不断开),返回小者头结点即可。T(N)=O(N+M),S(N)=O(N+M)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//l1或l2有null
if(l1==null){
return l2;
}else if(l2==null){
return l1;
}
//l1 l2均不为空,将小者作为头,剩下部分递归合并
else if(l1.val<l2.val){
l1.next=mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next=mergeTwoLists(l1,l2.next);
return l2;
}
}
}
方法二,将递归改写为迭代,提高执行效率,设置head作为头结点,pre作为最终结果的当前节点,然后分别遍历l1 l2,将较小元素接在pre后面,直到有一个链表为空,将剩余的非空链表接在pre后面即可。T(N)=O(N+M),S(N)=O(1),原地置换,不需要额外的存储空间。
(PS:占用空间比方法一还多,有点迷,可能算法执行上有点问题)
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//定义哨兵head以及最终链表的遍历节点pre
ListNode head=new ListNode(-1);
ListNode pre=head;
//l1 l2作为遍历原有两个链表的指针
while(l1!=null && l2!=null){
if(l1.val<l2.val){
pre.next=l1;
l1=l1.next;
}else{
pre.next=l2;
l2=l2.next;
}
pre=pre.next;
}
//将未遍历结束的链表直接接在最后面
pre.next=l1==null?l2:l1;
return head.next;
}
}
9、移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
方法一:暴力解法,遍历一次数组,每一次找到零,遍历后面寻找第一个不为0的数字,交换两个数字, T ( N ) = O ( N 2 ) , S ( N ) = O ( 1 ) T(N)=O(N^2),S(N)=O(1) T(N)=O(N2),S(N)=O(1)。
class Solution {
public void moveZeroes(int[] nums) {
for(int i=0;i<nums.length-1;++i){
//交换0和之后第一个不为0的数字
if(nums[i]==0){
for(int j=i+1;j<nums.length;++j){
if(nums[j]!=0){
nums[i]=nums[j];
nums[j]=0;
break;
}
}
}
}
}
}
方法二,设置指针lastFoundNotZeroIndex=0,只是上一个(最后一个非0元素的位置),遍历数组,将后边非0元素写给前边(不是交换!),最后将lastFoundNotZeroIndex及之后都设置为0即可, T ( N ) = O ( N ) , S ( N ) = O ( 1 ) T(N)=O(N),S(N)=O(1) T(N)=O(N),S(N)=O(1)。
注意,因为lastFoundNotZeroIndex与i都从0开始,所以不会出现非0覆盖非0的情况,最多会覆盖自身,然后后移,所以lastFoundNotZeroIndex之前必定都是非0数字。 其实很好理解,举个特殊情况全为0,全部需要赋0就是了。
class Solution {
public void moveZeroes(int[] nums) {
int lastFoundNotZeroIndex=0;
//将非0数字移动到前边
for(int i=0;i<nums.length;++i){
if(nums[i]!=0){
nums[lastFoundNotZeroIndex++]=nums[i];
}
}
//后半部分赋0
for(int i=lastFoundNotZeroIndex;i<nums.length;++i){
nums[i]=0;
}
}
}
方法三,快慢指针,慢指针之前都是非零元素,快指针与慢指针之间是0元素,每次快非0时,与慢交换,慢前进,准备覆盖下一个0值或者覆盖自身(原理同方法二), T ( N ) = O ( N ) , S ( N ) = O ( 1 ) T(N)=O(N),S(N)=O(1) T(N)=O(N),S(N)=O(1)。
class Solution {
public void moveZeroes(int[] nums) {
//快慢指针组合使用,每次快指针必定前进
for(int slow=0,fast=0;fast<nums.length;++fast){
//快指针不为0时,交换,慢指针也前进,准备覆盖下一个
if(nums[fast]!=0){
int temp=nums[slow];
nums[slow]=nums[fast];
nums[fast]=temp;
slow++;
}
}
}
}
10、把二叉搜索树转换为累加树
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。
例如:
输入: 二叉搜索树:
5
/ \
2 13
输出: 转换为累加树:
18
/ \
20 13
方法一:利用二叉搜索树的特点,左边都小于根节点的值,右边都大于根节点的值,进行逆中序遍历,先以起始值加上遍历右子树作为和,然后更新根节点,再以根节点为起始值加上遍历左子树作为和,最后将三个部分加在一起更新根节点。T(N)=O(N),S(N)=O(N)。
注意:根节点需要更新两次,而且遍历右子树一定是从起始值开始,而不是每次都从0开始。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode convertBST(TreeNode root) {
if(root==null){
return null;
}
inverseMiddleOrder(root,0);
return root;
}
//逆中序遍历求累加值
public int inverseMiddleOrder(TreeNode root,int initSum){
if(root==null){
return 0;
}
//先右子树递归
int rightSum=inverseMiddleOrder(root.right,initSum);
//记录root.val,并更新
int oldRootValue=root.val;
root.val=rightSum+oldRootValue+initSum;
//最后左递归
int leftSum=inverseMiddleOrder(root.left,root.val);
return leftSum+oldRootValue+rightSum;
}
}
方法二,使用全局遍历记录值,使用回溯。(这不就是递归吗…,倒是挺好理解的)。T(N)=O(N),S(N)=O(N)。
class Solution {
private int sum;
public TreeNode convertBST(TreeNode root) {
if(root!=null){
convertBST(root.right);
sum+=root.val;
root.val=sum;
convertBST(root.left);
}
return root;
}
}
11、找到所有数组中消失的数字
给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。
示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[5,6]
方法一,先排序,然后遍历数组,将num[i]放在num[i]-1的位置上,即1nums[0],2nums[1]…但是要注意,交换一次后位置i并不一定是应该有的数字,所以不能交换一次,应该交换到num[i]出现在i和nums[nums[i]-1]上,即出现两次,无需再次交换后,直到此时才自增i,向后遍历。极端情况,每次都会交换n次,T(N)=O(N^2),S(N)=O(1)。
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
Arrays.sort(nums);
List<Integer> result=new ArrayList<>();
//将num[i]放到num[i]-1的位置,i=num[i]-1
for(int i=0;i<nums.length;){
//未在指定位置,交换
//可能需要交换多次
//相等无需交换时才向后推进(2个重复)
int temp=nums[i];
if(temp!=nums[temp-1]){
nums[i]=nums[temp-1];
nums[temp-1]=temp;
}else{
++i;
}
}
//再次遍历,寻找不在指定位置的数字
for(int i=0;i<nums.length;++i){
if(nums[i]-1!=i){
result.add(i+1);
}
}
return result;
}
}
12、路径总和III
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
返回 3。和等于 8 的路径有:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
方法一,递归解法,总路径条数=根节点为起点的条数+左右子树为起点的条数。再编写另外一个函数来求以当前节点为起点的条数,因为当前节点为起点,可以分别向左右子树搜寻sum-root.val的和的路径条数。
T(N)=O(N^2),S(N)=O(H)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//以当前为起点,或者左右子树为起点
public int pathSum(TreeNode root, int sum) {
if(root==null){
return 0;
}
else{
return curAsRootPathSum(root,sum)
+pathSum(root.left,sum)+pathSum(root.right,sum);
}
}
//以当前为起点的路径总数
public int curAsRootPathSum(TreeNode root,int sum){
if(root==null){
return 0;
}
int pathSum=0;
if(root.val==sum){
pathSum++;
}
//当前为起点,向左右子树分别搜寻剩余的和
return pathSum+curAsRootPathSum(root.left,sum-root.val)
+curAsRootPathSum(root.right,sum-root.val);
}
}
13、买卖股票的最佳时机
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
方法一,暴力解法,两个for循环遍历数组元素及之后的元素,求出股票利润最大值。T(N)=O(N^2),S(N)=O(1)。
class Solution {
public int maxProfit(int[] prices) {
int result=0;
for(int i=0;i<prices.length-1;++i){
for(int j=i+1;j<prices.length;++j){
if(prices[j]-prices[i]>result){
result=prices[j]-prices[i];
}
}
}
return result;
}
}
方法二,
将示例数组绘出,发现只要找到谷底以及谷底之后的最大值,就可以找到最大利润。定义minPrice以及maxProfit,遍历数组随时更新minPrice,并将当前price与minPrice计算profit,进而确定maxProfit。T(N)=O(N),S(N)=T(1)。
class Solution {
public int maxProfit(int[] prices) {
int minPrice=Integer.MAX_VALUE;
int maxProfit=0;
int tempProfit=0;
for(int i=0;i<prices.length;++i){
//更新最小价格
if(prices[i]<minPrice){
minPrice=prices[i];
}
//更新最大利润
tempProfit=prices[i]-minPrice;
if(tempProfit>maxProfit){
maxProfit=tempProfit;
}
}
return maxProfit;
}
}
14、相交链表
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
方法一,如果两个链表相交,那么相交点之后的长度是相同的.
我们需要做的事情是,让两个链表从同距离末尾同等距离的位置开始遍历。这个位置只能是较短链表的头结点位置。为此,我们必须消除两个链表的长度差。
1)指针 pA 指向 A 链表,指针 pB 指向 B 链表,依次往后遍历
2)如果 pA 到了末尾,则 pA = headB 继续遍历
3)如果 pB 到了末尾,则 pB = headA 继续遍历
4)比较长的链表指针指向较短链表head时,长度差就消除了。
如此,只需要将最短链表遍历两次即可找到位置。
T(N,M)=O(M+N),S(M,N)=O(1)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null | headB==null){
return null;
}
ListNode pA=headA,pB=headB;
while(pA!=pB){
pA=pA==null?headB:pA.next;
pB=pB==null?headA:pB.next;
}
return pA;
}
}
方法二,先计算A B链表的长度,将长链表向后移动长度差个单位,然后同步移动,直到指向同一节点。T(N,M)=O(N+M),S(N)=T(1)。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null | headB==null){
return null;
}
ListNode pA=headA,pB=headB;
int aLength=0,bLength=0;
//计算A B的长度
while(pA!=null){
++aLength;
pA=pA.next;
}
while(pB!=null){
++bLength;
pB=pB.next;
}
pA=headA;
pB=headB;
//移动长链表使其位于同一起点
if(aLength>bLength){
for(int i=0;i<aLength-bLength;++i){
pA=pA.next;
}
}else{
for(int j=0;j<bLength-aLength;++j){
pB=pB.next;
}
}
while(pA!=pB){
pA=pA.next;
pB=pB.next;
}
return pA;
}
}
方法三,暴力解法,双循环寻找相交的节点。T(N,M)=O(MN),S(N,M)=O(1)。很耗时,强烈不推荐。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null || headB==null){
return null;
}
//暴力解法,双重循环
ListNode pA=headA;
while(pA!=null){
ListNode pB=headB;
while(pB!=null){
if(pA==pB){
return pA;
}
pB=pB.next;
}
pA=pA.next;
}
return null;
}
}
15、最小栈
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) -- 将元素 x 推入栈中。
pop() -- 删除栈顶的元素。
top() -- 获取栈顶元素。
getMin() -- 检索栈中的最小元素。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
方法一,两个Stack stack minStack来实现最小栈,stack正常操作,开始将第一个数压入minStack,之后每次push,当x<=minStack.peek,压入;每次pop,当两个栈栈顶相等,同时出栈,否则只stack栈顶出栈。
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
/** initialize your data structure here. */
public MinStack() {
stack=new Stack<>();
minStack=new Stack<>();
}
public void push(int x) {
stack.push(x);
//非空直接记录为最小值
//更新,使栈顶总为当前最小值
if(minStack.isEmpty()){
minStack.push(x);
}
else if(x<=minStack.peek()){
minStack.push(x);
}
}
public void pop() {
int tmpPop=stack.pop();
//两个栈顶相等,均弹出最小值,否则只弹出stack
if(tmpPop==minStack.peek()){
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
16、对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
方法一,递归。T(N)=O(N),S(N)=T(N)
递归结束条件:
- 都为空指针则返回 true
- 只有一个为空则返回 false
递归过程:
-
判断两个指针当前节点值是否相等
-
判断 A 的右子树与 B 的左子树是否对称
-
判断 A 的左子树与 B 的右子树是否对称
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return isMirror(root,root);
}
public boolean isMirror(TreeNode t1,TreeNode t2){
//均为空,对称
if(t1==null && t2==null){
return true;
}
//一个为空,不对称
if(t1==null || t2==null){
return false;
}
//先比较节点值,再循环比较左右子树
return (t1.val == t2.val)
&& isMirror(t1.left,t2.right)
&& isMirror(t1.right,t2.left);
}
}
方法二,回溯。使用队列来存储两个root节点,然后出队两次,比较是否对称,之后再依次将t1.left t2.right t1.right t2.left入队,循环直到队列为空。看起来,思想跟递归是一样的。
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
queue.add(root);
while(!queue.isEmpty()){
TreeNode t1=queue.poll();
TreeNode t2=queue.poll();
//均为空,对称
//继续判断,而非返回true
if(t1==null && t2==null){
continue;
}
//一为空,不对称
if(t1==null || t2==null){
return false;
}
//值不等,不对称
//不能用==。因为当前值相等,子树可能并不对称
if(t1.val != t2.val){
return false;
}
//t1t2子树依次入队
queue.add(t1.left);
queue.add(t2.right);
queue.add(t1.right);
queue.add(t2.left);
}
return true;
}
}
17、最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
方法一,贪心算法。
使用单个数组作为输入来查找最大(或最小)元素(或总和)的问题,贪心算法是可以在线性时间解决的方法之一。每一步都选择最佳方案,到最后就是全局最优的方案。T(N)=O(N),S(N)=O(N).
遍历数组并在每个步骤中更新:当前元素、当前元素位置的最大和、迄今为止的最大和。
class Solution {
public int maxSubArray(int[] nums) {
int curSum=nums[0];
int maxSum=nums[0];
for(int i=1;i<nums.length;++i){
//加上nums[i]之后若更大,则更新
//否则从nums[i]重新开始
curSum=Math.max(curSum+nums[i],nums[i]);
maxSum=Math.max(curSum,maxSum);
}
return maxSum;
}
}
方法二,暴力解法。T(N)=)(N^2),S(N)=O(1).
class Solution {
public int maxSubArray(int[] nums) {
int maxSum=nums[0];
for(int i=0;i<nums.length;++i){
int curSum=0;
for(int j=i;j<nums.length;++j){
curSum+=nums[j];
if(curSum>maxSum){
maxSum=curSum;
}
}
}
return maxSum;
}
}
18、爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
方法一,斐波拉契数列,递推公式 F ( N ) = F ( N − 1 ) + F ( N − 2 ) F(N)=F(N-1)+F(N-2) F(N)=F(N−1)+F(N−2),强行递归。 T ( N ) = O ( ( 1 + 5 2 ) N ) , S ( N ) = T ( N ) 。 T(N)=O(\left (\frac{1+\sqrt{5}}{2} \right )^{N}),S(N)=T(N)。 T(N)=O((21+5)N),S(N)=T(N)。
class Solution {
public int climbStairs(int n) {
if(n==1){
return 1;
}
if(n==2){
return 2;
}
return climbStairs(n-1)+climbStairs(n-2);
}
}
方法二,将斐波拉契数列从递归改写为迭代,动态规划。
本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
- 爬上 n-1阶楼梯的方法数量。因为再爬1阶就能到第n阶
- 爬上 n-2阶楼梯的方法数量,因为再爬2阶就能到第n阶
所以我们得到公式 dp[n] = dp[n-1] + dp[n-2]
同时需要初始化 dp[0]=1和 dp[1]=1
class Solution {
public int climbStairs(int n) {
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;++i){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
19、两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
方法一,暴力解法。T(N)=O(N^2),S(N)=T(1)。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result =new int[]{-1,-1};
for(int i=0;i<nums.length-1;++i){
for(int j=i+1;j<nums.length;++j){
if(nums[i]+nums[j]==target){
result[0]=i;
result[1]=j;
return result;
}
}
}
return result;
}
}
方法二,两遍哈希表。
在第一次迭代中,我们将每个元素的值和它的索引添加到表中。然后,在第二次迭代中,我们将检查每个元素所对应的目标元素(target - nums[i])是否存在于表中。注意,该目标元素不能是 nums[i] 本身!
T(N)=O(N),我们把包含有 n个元素的列表遍历两次。由于哈希表将查找时间缩短到 O(1),所以时间复杂度为 (n)。
S(N)=O(N),所需的额外空间取决于哈希表中存储的元素数量,该表中存储了 n 个元素。
class Solution {
public int[] twoSum(int[] nums, int target) {
int length=nums.length;
Map<Integer,Integer> map=new HashMap<>(length);
for(int i=0;i<length;++i){
map.put(nums[i],i);
}
for(int j=0;j<length;++j){
int otherTarget=target-nums[j];
//保证寻找的另一个数不在同一个位置,如2+2=4这种
if(map.containsKey(otherTarget) && map.get(otherTarget)!=j){
return new int[]{j,map.get(otherTarget)};
}
}
throw new IllegalArgumentException("找不到这两个整数。");
}
}
方法三,一遍哈希表。
第一次将元素加入哈希表中时,先判断是否存在tarhet-nums[i]的元素,存在直接返回,否则加入nums[i]。巧妙之处,避免了两个相同的数字出现在同一位置的问题。
class Solution {
public int[] twoSum(int[] nums, int target) {
int length=nums.length;
Map<Integer,Integer> map=new HashMap<>(length);
for(int i=0;i<length;++i){
int otherTarget=target-nums[i];
//加入HahsMap的同时进行遍历
if(map.containsKey(otherTarget)){
return new int[]{map.get(otherTarget),i};
}
//先判断再加入,避免同样数字出现在同一位置的问题
map.put(nums[i],i);
}
throw new IllegalArgumentException("找不到这两个整数。");
}
}
20、二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
方法一,定义树的深度为直线数目,所以一棵树深度为左右子树深度较大值+1。分析可知,树的直径若以当前为根节点,是左子树深度+右子树深度,但是直径所在根节点不一定以当前节点,可能出现在某棵子树中,使用ans来记录,每次计算树的深度是,动态更新ans。T(N)=O(N),S(N)=O(N)。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//最长半径根节点可能不在root,使用ans记录
int ans;
public int diameterOfBinaryTree(TreeNode root) {
ans=0;
depth(root);
return ans;
}
//求二叉树深度
public int depth(TreeNode node){
if(node==null){
return 0;
}
else{
int lDepth=depth(node.left);
int rDepth=depth(node.right);
ans=Math.max(ans,lDepth+rDepth);
return Math.max(lDepth,rDepth)+1;
}
}
}
21、环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
方法一,使用HashSet来存储各个ListNode,加入前先判断节点是否已经存在,存在,则有环,返回true(不会出现死循环);不存在则加入set,继续下一个节点;循环结束后仍未返回,说明无环,返回false;T(N)=O(N),S(N)=O(N)。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set=new HashSet<>();
while(head!=null){
//set中已经含有节点,有环
if(set.contains(head)){
return true;
}
else{
set.add(head);
}
head=head.next;
}
return false;
}
}
方法二,快慢指针。
慢指针每次走一步,快指针每次走两步,如果存在环则必定会遇上。
不存在环,则快指针迟早会走到null。值得注意的一点是,必须考虑到是否能够向前走,应该先进行fast.next和fast.next.next有效性的判断。
复杂度分析
●时间复杂度: O(n),让我们将n设为链表中结点的总数。为了分析时间复杂度, 我们分别考虑下面两种情况。
。链表中不存在环:
快指针将会首先到达尾部,其时间取决于列表的长度,也就是O(n)。
。链表中存在环:
我们将慢指针的移动过程划分为两个阶段:非环部分与环形部分: .
1.慢指针在完非环部分阶段后将进入环形部分:此时,快指针已经进入环中迭代次数=非环部分长度= N
2.两个指针都在环形区域中:考虑两个在环形赛道上的运动员-快跑者每次移动两步而慢跑者每次只移动一步。速度的差值为1,因此需要经过二者之间距离次循环后,快跑者可以追上慢跑速度差值者。这个距离几乎就是"环形部分长度K"且速度差值为1,我们得出这样的结论迭代次数=近似于"环形部分长度K".
因此,在最糟糕的情形下,时间复杂度为O(N + K),也就是O(n)。
●空间复杂度: O(1),我们只使用了慢指针和快指针两个结点,所以空间复杂度为O(1)。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow=head,fast=head;
while(fast!=null){
//验证fast指针下一步的有效性
if(fast.next==null || fast.next.next==null){
return false;
}
slow=slow.next;
fast=fast.next.next;
//有环,相遇,返回
if(slow==fast){
return true;
}
}
//快指针到达结尾,无环
return false;
}
}
22、打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
方法一,动态规划。在第i间,能偷窃到的最大金额,是前i-1间的最大金额,和前i-2间最大金额加上第i间的金额,初始条件为dp[0]=nums[0],dp[1]=Math.max(nums[0],nums[1])。但是要注意nums下标的有效性问题,还要考虑空数组的问题。
T(N)=O(N),S(N)=O(1)。
class Solution {
public int rob(int[] nums) {
if(nums.length==0){
return 0;
}
if(nums.length==1){
return nums[0];
}
int[] dp=new int[nums.length];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<nums.length;++i){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];
}
}
23、有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
方法一,因为都是和最近的左括号匹配,所以可以使用栈来解决。每次遇到左括号直接入栈,遇到右括号切栈不为空时,将匹配的左括号与栈顶比较,不等则返回false。如果遍历完整个String,栈不为空,说明不匹配,返回false,否则返回true。
T(N)=O(N),S(N)=T(N)。因为需要遍历一遍String,极端情况下会全是左括号,需要占用N个空间。
class Solution {
public boolean isValid(String s) {
if(s.length()==0){
return true;
}
Stack<Character> stack=new Stack<>();
for(char tmp:s.toCharArray()){
switch(tmp){
//栈不为空,搜索栈顶是否为匹配的左括号
case ')':{
if(!stack.isEmpty() && '('==stack.peek()){
stack.pop();
}
else{
return false;
}
break;
}
case ']':{
if(!stack.isEmpty() && '['==stack.peek()){
stack.pop();
}
else{
return false;
}
break;
}
case '}':{
if(!stack.isEmpty() && '{'==stack.peek()){
stack.pop();
}
else{
return false;
}
break;
}
//左括号直接入栈
default:{
stack.push(tmp);
break;
}
}
}
//遍历后的判断
if(!stack.isEmpty()){
return false;
}
return true;
}
}
24、回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
方法一,将链表转化为ArrayList来存储,然后从首尾两端向中间遍历,若出现不相等,返回false,遍历结束后则返回true。
T(N)=O(N),S(N)=O(N)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
if(head==null){
return true;
}
//将链表改用ArrayList存储
List<Integer> list=new ArrayList<>();
while(head!=null){
list.add(head.val);
head=head.next;
}
//从两端开始向中间遍历
for(int i=0,j=list.size()-1;i<j;++i,--j){
if(!list.get(i).equals(list.get(j))){
return false;
}
}
return true;
}
}
方法二,使用快慢指针slow + fast遍历链表,并使用pre + prepre来保存先前前半部分节点并翻转,随后从pre和slow分别向两边开始遍历,判断是否是回文链表。需要注意总节点数量为单双数的情况,若总结点为单数,需要让slow再次前进,跳至n+2处,随后在开始向两边遍历。
T(N)=O(N),S(N)=T(1)。
class Solution {
public boolean isPalindrome(ListNode head) {
//空链表或者只有一节,是回文
if(head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
ListNode pre = head, prepre = null;
//快指针指向末尾(总数2n+1)或者null(总数2n),
//均前进了2n个单位
//慢指针指向第n+1个节点
while(fast != null && fast.next != null) {
//保存slow
pre = slow;
//快慢指针前进
slow = slow.next;
fast = fast.next.next;
//翻转前边slow遍历过的链表
pre.next = prepre;
prepre = pre;
}
//总节数为单数2n+1,slow再次后移,n+1->n+2
if(fast != null) {
slow = slow.next;
}
//pre slow分别向两边遍历
while(pre != null && slow != null) {
if(pre.val != slow.val) {
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}
}
25、最短无序连续子数组
给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
你找到的子数组应是最短的,请输出它的长度。
示例 1:
输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
说明 :
输入的数组长度范围在 [1, 10,000]。
输入的数组可能包含重复元素 ,所以升序的意思是<=。
方法一,Arrays.sort排序产生新的排序数组,然后比较排序后与原数组,找出无序连续子数组的起始结束下标。注意可能有原数组有序的情况,遍历一遍确定start后,start==nums.length-1 && nums[start]==numsCopy[start]则返回0,否则继续找出end并计算返回子数组长度。或者遍历一次,每次比较两次,即时更新,在最后返回的时候判断start和end的关系,决定最后的返回值。
T(N)=O(NlgN),S(N)=O(N)。
class Solution {
public int findUnsortedSubarray(int[] nums) {
int[] numsCopy=nums.clone();
Arrays.sort(nums);
//找出无序连续子数组起始下标
int length=nums.length;
int start=0,end=length-1;
for(;start<length;++start){
if(nums[start]!=numsCopy[start]){
break;
}
}
//原数组有序,排序后不变
if(start == length-1 && nums[start] == numsCopy[start]){
return 0;
}
//找出无序连续子数组结束下标
for(;end>start-1;--end){
if(nums[end]!=numsCopy[end]){
break;
}
}
return end-start+1;
}
}
class Solution {
public int findUnsortedSubarray(int[] nums) {
int[] numsCopy=nums.clone();
Arrays.sort(nums);
//找出无序连续子数组起始结束下标
int length=nums.length;
int start=length-1,end=0;
for(int i=0;i<length;++i){
if(nums[i]!=numsCopy[i]){
start=Math.min(i,start);
end=Math.max(i,end)
}
}
return end-start>0?end-start+1:0;
}
}
方法二,同时从前往后和从后往前遍历,分别得到排序数组的右边界和左边界;
寻找右边界:从前往后遍历的过程中,用max_num记录遍历过的最大值,如果max_num大于当前的nums[i],说明nums[i]的位置不正确,应该属于需要排序的数组,因此将右边界更新为i,然后更新max_num;这样最终可以找到需要排序的数组的右边界,右边界之后的元素都大于max_num;
寻找左边界:从后往前遍历的过程中,用min_num记录遍历过的最小值,如果min_num小于当前的nums[j],说明nums[j]的位置不正确,应该属于需要排序的数组,因此将左边界更新为j,然后更新min_num;这样最终可以找到需要排序的数组的左边界,左边界之前的元素都小于min_num;
T(N)=O(N),S(N)=O(1)。
class Solution {
public int findUnsortedSubarray(int[] nums) {
int length=nums.length;
int max_num=nums[0],end=0;
int min_num=nums[length-1],start=length-1;
//若从左到右递增,更新max_num,当前部分无需排序
//否则说明nums[i]不在有序的正确位置,更新end
for(int i=0;i<length;++i){
if(nums[i]>=max_num){
max_num=nums[i];
}
else{//nums[i]<max_num
end=i;
}
}
//若从右到左递减,更新min_num,当前部分无需排序
//否则说明nums[j]不在有序的正确位置,更新start
for(int j=length-1;j>-1;--j){
if(nums[j]<=min_num){
min_num=nums[j];
}
else{//nums[j]>min_num
start=j;
}
}
return end-start>0?end-start+1:0;
}
}
热题100中等难度
26、子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
方法一,使用二进制位来代表各个元素是否被选中,对于实例,共有2^3=(1>>3)=8种情况,在每一种情况中,((i >> j) & 1)==1依次判断从低位到高位是否为1,对应是否被选中,比如子集[1,3]时,i=5(10)=101(2),((101 >> 0) & 1)==1为true,选中nums[0]=1;((101 >> 1) & 1)==1为false,不选中nums[1]=2;((101 >> 2) & 1)==1为true,选中nums[2]=3;本次子集选择情况为[1,3]。
复杂度分析,共有2N种情况,每种情况需要遍历一次nums,T(N)=O(N·2N)
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> subsets=new ArrayList<>();
int length=nums.length;
//相当于2^length次情况,位移效率更高
for(int i=0;i<(1<<length);++i){
List<Integer> set=new ArrayList<>();
//对于第i种组合,依次判断从低位到高位是否为1
//即从nums[0]~nums[length-1]是否包含在当前子集中
for(int j=0;j<length;++j){
if(((i>>j)&1)==1){
set.add(nums[j]);
}
}
subsets.add(set);
}
return subsets;
}
}
27、比特位计数
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
示例 1:
输入: 2
输出: [0,1,1]
示例 2:
输入: 5
输出: [0,1,1,2,1,2]
进阶:
给出时间复杂度为O(n*sizeof(integer))的解答非常容易。但你可以在线性时间O(n)内用一趟扫描做到吗?
要求算法的空间复杂度为O(n)。
你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。
方法一,暴力解题。T(N)=O(NlgN),S(N)=O(N)。
还有一种求1的位数高效方法,剑指offer里面有提到过,不太好想。
private int popcount(int x) {
int count;
for (count = 0; x != 0; ++count)
x &= x - 1; //zeroing out the least significant nonzero bit
return count;
}
class Solution {
public int[] countBits(int num) {
int[] result=new int[num+1];
for(int i=0;i<=num;++i){
int curBits=0;
for(int j=i;j!=0;j>>=1){
if((j&1)==1){
curBits++;
}
}
result[i]=curBits;
}
return result;
}
}
方法二,归纳总结递推公式。T(N)=O(N),S(N)=O(N).
0 => 0
1 => 1
2 => 10
3 => 11
4 => 100
5 => 101
6 => 110
7 => 111
8 => 1000
· 归纳
设 dp(n) 为数字n二进制中1的个数
n为奇数,dp(n) = dp(n-1) + 1
n为偶数,dp(n) = dp(n/2)
class Solution {
public int[] countBits(int num) {
int[] result=new int[num+1];
for(int i=1;i<=num;++i){
if((i&1)==1){
result[i]=result[i-1]+1;
}
else{
result[i]=result[i>>1];
}
}
return result;
}
}
方法三,动态规划+最高有效位,这个真的不太好看懂。T(N)=O(N),S(N)=O(N).
利用已有的计数结果来生成新的计数结果。
对于pop count P(x)P(x),我们有以下的状态转移函数:
P(x + b) = P(x) + 1, b = 2^m > x
有了状态转移函数,我们可以利用动态规划从 0开始生成所有结果。
public class Solution {
public int[] countBits(int num) {
int[] ans = new int[num + 1];
int i = 0, b = 1;
// [0, b) is calculated
while (b <= num) {
// generate [b, 2b) or [b, num) from [0, b)
while(i < b && i + b <= num){
ans[i + b] = ans[i] + 1;
++i;
}
i = 0; // reset i
b <<= 1; // b = 2b
}
return ans;
}
}
28、全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
回溯算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jCxGxK2f-1598193666867)(…/AppData/Roaming/Typora/typora-user-images/image-20200111183136898.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qXmyRSSf-1598193666868)(…/AppData/Roaming/Typora/typora-user-images/image-20200111183247447.png)]
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res=new ArrayList<>();
int[] visited=new int[nums.length];
backtrack(res,new ArrayList<Integer>(),nums,visited);
return res;
}
public void backtrack(List<List<Integer>> res,
List<Integer> tmp,int[] nums,int[] visited){
//满了,保存一次
if(tmp.size()==nums.length){
res.add(new ArrayList<Integer>(tmp));
}
for(int i=0;i<nums.length;++i){
//已访问过,不再访问
if(visited[i]==1) continue;
//访问nums[i],继续访问
visited[i]=1;
tmp.add(nums[i]);
backtrack(res,tmp,nums,visited);
//回溯,退回上一步状态
visited[i]=0;
tmp.remove(tmp.size()-1);
}
}
}
29、括号生成(未做)
30、二叉树的中序遍历
给定一个二叉树,返回它的中序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,3,2]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
方法一,递归算法,按照左-根-右的顺序来遍历,使用全局变量来记录最终结果。递归结束条件为遍历到空节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root==null){
return res;
}
inorderTraversal(root.left);
res.add(root.val);
inorderTraversal(root.right);
return res;
}
}
方法二,迭代。使用Stack来按照根-左的顺序存储节点,然后依次出栈,会按照左-根顺序出栈,并且会将无右子树的左子树访问完成,访问其根节点(出栈的根节点),再访问出栈的根节点的右节点,依次循环即可。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>();
Stack<TreeNode> nodes=new Stack<>();
TreeNode curr=root;
while(curr!=null || !nodes.isEmpty()){
//根-左入栈
while(curr!=null){
nodes.push(curr);
curr=curr.left;
}
//左-根出栈
curr=nodes.pop();
res.add(curr.val);
curr=curr.right;
//下一步会从有右子树的右子树开始
}
return res;
}
}
31、组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
方法一,回溯法。
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
//排序,再依次查找
Arrays.sort(candidates);
backtrack(candidates,target,res,0,new ArrayList<Integer>());
return res;
}
//回溯
public void backtrack(int[] candidates,int target,List<List<Integer>> res,
int i,ArrayList<Integer> tmp_list){
if(target<0){
return;
}
//找到一种组合,添加
if(target==0){
//deep clone,不能把引用传递进去
res.add(new ArrayList<Integer>(tmp_list));
}
//从下标i开始寻找
for(int start=i;start<candidates.length;++start){
if(target<candidates[start]){
return;
}
//当前位置加入,并从当前位置继续向后寻找
//也就意味着可以重复选择某个数字
tmp_list.add(candidates[start]);
backtrack(candidates,target-candidates[start],res,start,tmp_list);
//回溯,删除上一个数字
tmp_list.remove(tmp_list.size()-1);
}
}
}
32、二叉树展开为链表
给定一个二叉树,原地将它展开为链表。
例如,给定二叉树
1
/ \
2 5
/ \ \
3 4 6
将其展开为:
1
\
2
\
3
\
4
\
5
\
6
方法一,递归。虽然不难,还是要谨慎一点,考虑到各种情况,不是直接把左子树挪过去就行了,右子树要接在合适的位置,原有左子树也要置空。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public void flatten(TreeNode root) {
if(root==null){
return;
}
//左子树不为空,展开
if(root.left!=null){
flatten(root.left);
}
//右子树不为空,展开
if(root.right!=null){
flatten(root.right);
}
//原左子树展开后接在右子树
//原左子树置空
TreeNode oriLeft=root.left,oriRight=root.right;
root.right=oriLeft;
root.left=null;
//原右子树展开后接在,左子树展开后的最右子树
while(root.right!=null){
root=root.right;
}
root.right=oriRight;
return;
}
}
33、旋转图像
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
示例 2:
给定 matrix =
[
[ 5, 1, 9,11],
[ 2, 4, 8,10],
[13, 3, 6, 7],
[15,14,12,16]
],
原地旋转输入矩阵,使其变为:
[
[15,13, 2, 5],
[14, 3, 4, 1],
[12, 6, 8, 9],
[16, 7,10,11]
]
方法一,先将矩阵整体转置,然后每一行前后翻转即可。T(N)=O(N^2),S(N)=O(1).
拿到题目先回想线性代数的知识,别急着做题,慢慢来,比较快。
class Solution {
public void rotate(int[][] matrix) {
int n=matrix.length;
//先整体转置
for(int i=0;i<n;++i){
for(int j=i+1;j<n;++j){
int temp=matrix[i][j];
matrix[i][j]=matrix[j][i];
matrix[j][i]=temp;
}
}
//再对每一行进行翻转
for(int i=0;i<n;++i){
for(int j=0;j<n/2;++j){
int temp=matrix[i][j];
matrix[i][j]=matrix[i][n-1-j];
matrix[i][n-1-j]=temp;
}
}
}
}
方法二,旋转多个正方形。T(N)=O(N^2),S(N)=O(1).
i<(n+1)/2,负责将将每一行都遍历到(就是第一行每个元素都到)。
j<n/2,负责向内层逼近。
![image-20200114143159428](../AppData/Roaming/Typora/typora-user-images/image-20200114143159428.png)
class Solution {
public void rotate(int[][] matrix) {
int n=matrix.length;
for(int i=0;i<(n+1)/2;++i){
for(int j=0;j<n/2;++j){
int temp = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 -j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 -i];
matrix[j][n - 1 - i] = matrix[i][j];
matrix[i][j] = temp;
}
}
}
}
34、除自身以外数组的乘积
给定长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
示例:
输入: [1,2,3,4]
输出: [24,12,8,6]
说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。
进阶:
你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
方法一,两个数组分别存储当前元素左边右边累积乘积,分别从nums左右开始,初始都置为1。对于nums=[1,2,3,4],curLeftMul=[1,1,2,6],curRightMul=[24,12,4,1],curLeftMul与curRightMul对应数字相乘即为最终结果。
T(N)=O(N),S(N)=O(N).
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] curLeftMul=new int[nums.length];
int[] curRightMul=new int[nums.length];
curLeftMul[0]=1;curRightMul[nums.length-1]=1;
//当前左边累积乘积
for(int i=1;i<nums.length;++i){
curLeftMul[i]=curLeftMul[i-1]*nums[i-1];
}
//当前右边累积乘积
for(int i=nums.length-2;i>-1;--i){
curRightMul[i]=curRightMul[i+1]*nums[i+1];
}
//最终结果
for(int i=0;i<nums.length;++i){
curRightMul[i]*=curLeftMul[i];
}
return curRightMul;
}
}
方法二,思路同方法一,只不过再计算当前元素右边累积乘积时,不再需要记录所有,使用一个临时变量依次记录即可,因为只需要使用一次。缩减空间复杂度。T(N)=O(N),S(N)=O(1).
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] curLeftMul=new int[nums.length];
curLeftMul[0]=1;
//当前左边累积乘积
for(int i=1;i<nums.length;++i){
curLeftMul[i]=curLeftMul[i-1]*nums[i-1];
}
//curRightMul为依次右边累积乘积
//再将左边累积乘积乘进去
//不需要记录所有的右边累积乘积
int curRightMul=1;
for(int i=nums.length-1;i>-1;--i){
curLeftMul[i]*=curRightMul;
curRightMul*=nums[i];
}
return curLeftMul;
}
}
35、不同的二叉搜索树
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
方法一,动态规划。
假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数,则G(n) = f(1) + f(2) + f(3) + f(4) + … + f(n)
当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,则f(i) = G(i-1)*G(n-i)
综合两个公式可以得到 卡特兰数公式
G(n) = G(0)G(n-1)+G(1)(n-2)+…+G(n-1)*G(0)= ∑ i = 1 n G ( i − 1 ) ⋅ G ( n − i ) \sum_{i=1}^{n}G(i-1)\cdot G(n-i) ∑i=1nG(i−1)⋅G(n−i)
T(N)=O(N^2),S(N)=O(N).
class Solution {
public int numTrees(int n) {
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<n+1;++i){
for(int j=1;j<=i;++j){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
}
方法二,一样的思路,直接套用卡特兰数的公式求解, C 0 = 1 , C n + 1 = 2 ( 2 n + 1 ) n + 2 C n C_{0}=1,C_{n+1}=\frac{2(2n+1)}{n+2}C_{n} C0=1,Cn+1=n+22(2n+1)Cn.
T(N)=O(N),S(N)=O(1)。注意,中间计算结果用int
存储可能会溢出。
class Solution {
public int numTrees(int n) {
long res=1;
for(int i=0;i<n;++i){
res=res*2*(2*i+1)/(i+2);
}
return (int)res;
}
}
36、最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
方法一,动态规划。
状态定义:设 dp 为大小 m × n m \times n m×n矩阵,其中 dp[i] [j]的值代表直到走到 (i,j)的最小路径和。
题目要求,只能向右或向下走,换句话说,当前单元格(i,j) 只能从左方单元格 (i-1,j)(i−1,j) 或上方单元格 (i,j-1)(i,j−1) 走到,因此只需要考虑矩阵第一行和第一列的特殊情况,其他情况直接从左边和上边选取较小路径和继续加入当前路径即可。
T(N)=O(M * N),S(N)=O(M * N)。也可以省略dp矩阵,直接修改原矩阵,此时S(N)=O(1)。
class Solution {
public int minPathSum(int[][] grid) {
int m=grid.length,n=grid[0].length;
//int[][] dp=new int[m][n];
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
//起始位置grid[0][0]维持grid[0][0]不变
if(i==0 && j==0){
//dp[i][j]=grid[i][j];
continue;
}
//第一排,只能从左边过来
else if(i==0){
//dp[i][j]=dp[i][j-1]+grid[i][j];
grid[i][j]=grid[i][j-1]+grid[i][j];
}
//第一列,只能从上边过来
else if(j==0){
//dp[i][j]=dp[i-1][j]+grid[i][j];
grid[i][j]=grid[i-1][j]+grid[i][j];
}
//其他位置,选取左边和上边较小者过来
else{
//dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
grid[i][j]=Math.min(grid[i-1][j],grid[i][j-1])+grid[i][j];
}
}
}
//return dp[m-1][n-1];
return grid[m-1][n-1];
}
}
37、实现 Trie (前缀树)(未做)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
示例:
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");
trie.search("app"); // 返回 true
说明:
你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。
38、从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
方法一,哈哈,这题我会,剑指Offer里面见到过,前序从根开始,在中序找到根节点,左边为左子树中序,右边为右子树中序,再往前序找出左右子树的前序,递归即可完成树的构造。需要注意的地方就是,确定递归构造的起始和结束为止,左右子树的长度。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
//方法二
private Map<Integer,Integer> map=new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder==null||inorder==null){
return null;
}
if(preorder.length!=inorder.length){
return null;
}
//方法二
//for(int j=0;i<inorder.length;++j){
// map.put(inorder[j],j);
//}
return helper(preorder,0,preorder.length,
inorder,0,inorder.length);
}
public TreeNode helper(int[] preorder,int p_start,int p_end,
int[] inorder,int i_start,int i_end){
//前序遍历结束
if(p_start == p_end){
return null;
}
//构建根节点,位于前序之首
//并确定其在中序位置
int root_val=preorder[p_start];
TreeNode root=new TreeNode(root_val);
//找出中序遍历的根节点
//方法二
//int i_root_index=map.get(root_val);
int i_root_index=0;
for(int i=i_start;i<i_end;++i){
if(root_val==inorder[i]){
i_root_index=i;
break;
}
}
//递归构建左右子树
int left_num=i_root_index-i_start;
root.left=helper(preorder,p_start+1,p_start+1+left_num,
inorder,i_start,i_root_index);
root.right=helper(preorder,p_start+1+left_num,p_end,
inorder,i_root_index+1,i_end);
return root;
}
}
方法二,在方法一的基础上改进,减少递归栈带来的时间空间支出,首先将中序存储在HashMap中,每次确定根节点在中序中的位置提高效率。
39、排序链表
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
方法一,递归的归并排序。一共有分割合并两个过程,分割首先要使用slow fast两个指针遍历,找出链表中点断开,直到断成一个一个节点为止;合并时,就是简单的两个链表合并并排序,注意合并时遇到示例2的情况,会有落单的节点存在,所以一定要把落单节点考虑进去。还有一点需要注意,就是递归的位置,应该放在分割合并中间。T(N)=O(NlgN),普通的归并排序因为要开辟额外的数组空间,所以S(N)=O(N),链表有一个有点就是节点可以灵活地变化,所以这道题S(N)=O(1)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if(head==null || head.next==null){
return head;
}
//快慢指针找到中点,断开
//最后slow fast分别指向两段链表的开头
ListNode slow=head;
ListNode fast=head.next;
while(fast.next!=null && fast.next.next!=null){
slow=slow.next;
fast=fast.next.next;
}
fast=slow.next;
slow.next=null;
//左右两段递归排序完毕
ListNode sortLeft=sortList(head);
ListNode sortRight=sortList(fast);
//虚拟头结点值为0,等会儿会返回下一个节点
ListNode h=new ListNode(0);
ListNode res=h;
//合并链表并排序,老生常谈了
while(sortLeft!=null && sortRight!=null){
if(sortLeft.val<sortRight.val){
h.next=sortLeft;
sortLeft=sortLeft.next;
}
else{
h.next=sortRight;
sortRight=sortRight.next;
}
h=h.next;
}
//有一个链表还未合并完毕
//因为归并排序遇到总数为单的情况,会有落单的节点
h.next=sortLeft!=null?sortLeft:sortRight;
return res.next;
}
}
40、寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
1、不能更改原数组(假设数组是只读的)。
2、只能使用额外的 O(1) 的空间。
3、时间复杂度小于 O(n2) 。
4、数组中只有一个重复的数字,但它可能不止重复出现一次。
如果没有额外说明,这道题倒是蛮简单的,比如排序后从前到后两两比较,再比如判断是否包含在HashSet内后再加入,已经包含的肯定是重复的数字,但是加了这个说明的额外限制,这尼玛谁想的出来,这方法三名字就挺怪的,弗洛伊德的乌龟和兔子(循环解法)。
首先明确前提,整数的数组 nums中的数字范围是[1,n]。考虑一下两种情况:
如果数组中没有重复的数,以数组[1,3,4,2]为例,我们将数组下标n和数nums[n]建立一个映射关系f(n),
其映射关系n->f(n)为:
0->1
1->3
2->4
3->2
我们从下标为0出发,根据f(n)计算出一个值,以这个值为新的下标,再用这个函数计算,以此类推,直到下标超界。这样可以产生一个类似链表一样的序列。
0->1->3->2->4->null
如果数组中有重复的数,以数组[1,3,4,2,2]为例,我们将数组下标n和数nums[n]建立一个映射关系f(n),
其映射关系n->f(n)为:
0->1
1->3
2->4
3->2
4->2
同样的,我们从下标为0出发,根据f(n)计算出一个值,以这个值为新的下标,再用这个函数计算,以此类推产生一个类似链表一样的序列。
0->1->3->2->4->2->4->2->……
这里2->4是一个循环,那么这个链表可以抽象为下图:
从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射,这样,形成的链表就一定会有环路了,
综上
1.数组中有一个重复的整数 <> 链表中存在环
2.找到数组中的重复整数 <> 找到链表的环入口
至此,问题转换为142题。那么针对此题,快、慢指针该如何走呢。根据上述数组转链表的映射关系,可推出
142题中慢指针走一步slow = slow.next ==> 本题 slow = nums[slow]
142题中快指针走两步fast = fast.next.next ==> 本题 fast = nums[nums[fast]]
class Solution {
public int findDuplicate(int[] nums) {
//数组寻找重复数字
//转化为有环链表寻找重复数字问题
int slow=0;
int fast=0;
slow=nums[slow];
fast=nums[nums[fast]];
while(slow!=fast){
slow=nums[slow];
fast=nums[nums[fast]];
}
//找到相交部分的起点
int pre1=0;
int pre2=slow;
while(pre1!=pre2){
pre1=nums[pre1];
pre2=nums[pre2];
}
return pre1;
}
}
41、根据身高重建队列
假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
注意:
总人数少于1100人。
示例
输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
方法一,让我们从最简单的情况下思考,当队列中所有人的 (h,k)
都是相同的高度 h
,只有 k
不同时,解决方案很简单:每个人在队列的索引 index = k
。
即使不是所有人都是同一高度,这个策略也是可行的。因为个子矮的人相对于个子高的人是 “看不见” 的,所以可以先安排个子高的人。
我们先安排身高为 7 的人,将它放置在与 k 值相等的索引上;再安排身高为 6 的人,同样的将它放置在与 k 值相等的索引上。
该策略可以递归进行:
将最高的人按照 k 值升序排序,然后将它们放置到输出队列中与 k 值相等的索引位置上。
按降序取下一个高度,同样按 k 值对该身高的人升序排序,然后逐个插入到输出队列中与 k 值相等的索引位置上。
直到完成为止。
复杂度分析:
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2)。排序使用了
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN) 的时间,每个人插入到输出队列中需要
O
(
k
)
O(k)
O(k) 的时间,其中 k 是当前输出队列的元素个数。总共的时间复杂度为
O
(
∑
k
=
0
N
−
1
k
)
=
O
(
N
2
)
。
{O}\left({\sum\limits_{k = 0}^{N - 1}{k}}\right) = {O}(N^2)。
O(k=0∑N−1k)=O(N2)。
空间复杂
O
(
N
)
O(N)
O(N),输出队列使用的空间。
class Solution {
public int[][] reconstructQueue(int[][] people) {
//解题思路:先排序再插入
//核心思想:高个子先站好位,矮个子插入到K位置上,前面肯定有K个高个子
//矮个子再插到前面也满足K的要求
//[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
//按照先H高度降序,H相同则按照K升序
// [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
Arrays.sort(people,
(o1,o2)->o1[0]==o2[0]?o1[1]-o2[1]:o2[0]-o1[0]);
//遍历排序后的数组,根据K插入到K的位置
//一个一个插入。
// [7,0]
// [7,0], [7,1]
// [7,0], [6,1], [7,1]
// [5,0], [7,0], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
LinkedList<int[]> res=new LinkedList<>();
for(int[] i:people){
res.add(i[1],i);
}
//将list转化为int[][]
return res.toArray(people);
}
}
42、盛最多水的容器
给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例:
输入: [1,8,6,2,5,4,8,3,7]
输出: 49
方法一,暴力法,循环嵌套寻找出矩形面积的最大值(j-i)**Math.min(height[i],height[j])。T(N)=O(N^2),S(N)=O(1).
class Solution {
public int maxArea(int[] height) {
int maxCont=-1;
for(int i=0;i<height.length-1;++i){
for(int j=i+1;j<height.length;++j){
if(Math.abs(j-i)*Math.min(height[i],height[j])>maxCont){
maxCont=Math.abs(j-i)*Math.min(height[i],height[j]);
}
}
}
return maxCont;
}
}
方法二,这种方法背后的思路在于,两线段之间形成的区域总是会受到其中较短那条长度的限制。此外,两线段距离越远,得到的面积就越大。
我们在由线段长度构成的数组中使用两个指针,一个放在开始,一个置于末尾。 此外,我们会使用变量 maxarea 来持续存储到目前为止所获得的最大面积。 在每一步中,我们会找出指针所指向的两条线段形成的区域,更新 maxarea,并将指向较短线段的指针向较长线段那端移动一步。但是感觉这种方法,有点取巧,最好深入掌握一下证明的方法,可以考虑用反证法,不然人家一问就懵逼了。T(N)=O(N),S(N)=O(1).
class Solution {
public int maxArea(int[] height) {
int maxCont=-1;
for(int left=0,right=height.length-1;left<right;){
//更新最大容器体积
maxCont=Math.max(maxCont,Math.min(height[left],height[right])*(right-left));
//left边较小,想办法扩大,向右移动
if(height[left]<height[right]){
left++;
}
//right边较小,想办法扩大,向左移动
else{
right--;
}
}
return maxCont;
}
}
43、数组中的第K个最大元素
方法一,Arrays.sort()再返回nums[nums.length-k].
方法二,创建一个大顶堆,将所有数组中的元素加入堆中,并保持堆的大小小于等于 k。这样,堆中就保留了前 k 个最大的元素。这样,堆顶的元素就是正确答案。
像大小为 k 的堆中添加元素的时间复杂度为 O(logk),我们将重复该操作 N 次,故总时间复杂度为 O(Nlogk)。
本方法优化了时间复杂度,但需要 O(k) 的空间复杂度。
class Solution {
public int findKthLargest(int[] nums, int k) {
//比较器将优先级队列指定为升序,即小顶堆,堆顶总是最小元素
PriorityQueue<Integer> queue=
new PriorityQueue<>((o1,o2)->o1-o2);
//堆元素多于k个时,弹出堆顶最小元素
for(int num:nums){
queue.offer(num);
if(queue.size()>k){
queue.poll();
}
}
//仅剩k个最大元素的堆顶,即第k个最大元素
return queue.peek();
}
}
44、二叉树的层级遍历
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
层级遍历不是很难,使用队列借助其先进先出的特性,root入队,出队,左右子树非空则入队,然后循环此过程,即可实现层级遍历。
这道题复杂之处在于,需要将每一层节点分开输出,因为每次入队的是一层所有的节点,所以先计算本层节点数,即队列大小,然后本层所有节点出队并记录节点值,同时将所有节点的左右子树入队(即下一层所有节点)。
因为将所有节点遍历一次,T(N)=O(N)。队列大小为层节点最多的数目,极端情况为完全二叉树,S(N)=O(N/2)=O(N)。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
Queue<TreeNode> queue=new LinkedList<>();
if(root==null){
return res;
}
queue.offer(root);
while(!queue.isEmpty()){
//将当前队列所有元素,即本层所有节点取出记录
//并将左右子树入队列
List<Integer> tmpRes=new ArrayList<>();
int curWidth=queue.size();
for(int i=0;i<curWidth;++i){
TreeNode curNode=queue.poll();
tmpRes.add(curNode.val);
if(curNode.left!=null){
queue.offer(curNode.left);
}
if(curNode.right!=null){
queue.offer(curNode.right);
}
}
res.add(tmpRes);
}
return res;
}
}
45、字母异位词分组
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
示例:
输入: ["eat", "tea", "tan", "ate", "nat", "bat"],
输出:
[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]
说明:
所有输入均为小写字母。
不考虑答案输出的顺序。
方法一,使用HashMap<String,List>来存储排序后的异位词,以及异位词组成的集合,将原始String[]中的String转化为char[]再排序再转化为String就是排序后的异位词,若不包含此key则put(key,new ArrayList()),然后将key对应的list更新。
这种方法会将String[]遍历一遍O(N),对每个元素进行排序O(KlogK)(K是最长的String长度),T(N)=O(NKlogK)。S(N)=O(NK)。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List> map=new HashMap<>();
if(strs.length==0){
return new ArrayList<>();
}
for(String s:strs){
char[] arr=s.toCharArray();
//排序之后的String作为key
Arrays.sort(arr);
String key=String.valueOf(arr);
//首次初始化
if(!map.containsKey(key)){
map.put(key,new ArrayList());
}
//设置对应list
map.get(key).add(s);
}
return new ArrayList(map.values());
}
}
方法二,基本思路类似,但是关键思路修改,比如abbccc、bbaccc表示a出现1次b出现2次c出现3次,可以用"#1#2#3#0…#0"字符串表示。
相较于方法一,排序部分改成了遍历一次统计,T(N)=O(N)O(K)=O(NK),S(N)=O(NK)。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List> map=new HashMap<>();
if(strs.length==0){
return new ArrayList<>();
}
for(String s:strs){
char[] arr=s.toCharArray();
int[] letterCount=new int[26];
//表示26个英文字母出现次数的String作为key
for(char ch:arr){
letterCount[ch-'a']++;
}
StringBuffer sb=new StringBuffer("");
for(int num:letterCount){
sb.append("#");
sb.append(num);
}
String key=String.valueOf(sb);
//首次初始化
if(!map.containsKey(key)){
map.put(key,new ArrayList());
}
//设置对应list
map.get(key).add(s);
}
return new ArrayList(map.values());
}
}
46、回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。
示例 1:
输入: "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c".
示例 2:
输入: "aaa"
输出: 6
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".
注意:
输入的字符串长度不会超过1000。
方法一,动态规划。dp[j] [i]代表从str[j]到str[i]的是否是回文子串。要断定dp[j] [i]是否是回文子串,应该从短到长推断,dp[j+1] [i-1]为true且str[i] str[j]相等。
但是要考虑初始化以及单字符双字符的情况,即i-j<2。所以判断条件应该为
s.charAt[i]==s.charAt[j] && ( (i-j<2) || dp[j+1] [i-1] )。
复杂度分析,双循环判断回文子串数量,T(N)=O(N2).S(N)=O(N2)
class Solution {
public int countSubstrings(String s) {
int res=0;
boolean[][] dp=new boolean[s.length()][s.length()];
for(int i=0;i<s.length();++i){
for(int j=i;j>=0;j--){
//由短子串状态+扩充字符推断长子串状态
if( s.charAt(i)==s.charAt(j) && ( (i-j<2) || dp[j+1][i-1] ) ){
dp[j][i]=true;
++res;
}
}
}
return res;
}
}
47、前K个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
说明:
你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
方法一,使用HashMap<Integer,Integer>来记录元素以及出现次数O(N),然后维护最小堆PriorityQueue来记录HashMap中的key,将key对应的value作为权值,动态维护最小堆数量为K,超过后将堆顶即出现次数最少的key出堆,每插入一次O(logK),N次插入为O(NlogK)。然后将最小堆元素出堆依次压入List即可,注意此时是按出现次数由小到大排列的,可以选用LinkedList作为实现,然后调用Collections.reverse进行翻转,当然不翻转也没啥事。
复杂度分析T(N)=O(NlogK),S(N)=O(N),即建立HashMap所需的空间。
class Solution {
public List<Integer> topKFrequent(int[] nums, int k) {
//存储元素以及出现的次数
Map<Integer,Integer> map=new HashMap<>();
for(int num:nums){
if(!map.containsKey(num)){
map.put(num,1);
}
else{
map.put(num,map.get(num)+1);
}
}
//按照map的value升序加入,即维护value的最小堆
//将key对应的value作为权值
//堆顶总是出现最少次数的key,出栈最小,剩余出现次数多
PriorityQueue<Integer> heap=
new PriorityQueue<>((n1,n2)->map.get(n1)-map.get(n2));
//寻找前k个出现次数最多的
for(int key:map.keySet()){
heap.offer(key);
if(heap.size()>k){
heap.poll();
}
}
//构造输出,此时依次是出现次数由少到多的结果
List<Integer> res=new LinkedList<>();
while(!heap.isEmpty()){
res.add(heap.poll());
}
//翻转一下,链表很方便,不翻转也可以
Collections.reverse(res);
return res;
}
}
48、二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
![img](https://i-blog.csdnimg.cn/blog_migrate/194dc616212512f5efd6c8e72895ea22.png)
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
方法一,先深度遍历改树。当你遇到节点 p 或 q 时,返回一些布尔标记。该标志有助于确定是否在任何路径中找到了所需的节点。最不常见的祖先将是两个子树递归都返回真标志的节点。它也可以是一个节点,它本身是p或q中的一个,对于这个节点,子树递归返回一个真标志。
算法:
-
从根节点开始遍历树。
-
如果当前节点本身是 p 或 q 中的一个,我们会将变量 mid 标记为 true,并继续搜索左右分支中的另一个节点。
-
如果左分支或右分支中的任何一个返回 true,则表示在下面找到了两个节点中的一个。
-
如果在遍历的任何点上,左、右或中三个标志中的任意两个变为 true,这意味着我们找到了节点 p 和 q 的最近公共祖先。
T(N)=O(N),S(N)=O(N)。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private TreeNode res=null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
helper(root,p,q);
return res;
}
//判断cur是否为p q最近公共祖先
public boolean helper(TreeNode cur,TreeNode p,TreeNode q){
//到达终点
if(cur==null){
return false;
}
//判断左右子树和当前节点是否包含p q任一
int left=helper(cur.left,p,q)?1:0;
int right=helper(cur.right,p,q)?1:0;
int mid=(cur==p || cur==q)?1:0;
//左右子树 根节点3折出现2次,cur即为最近公共祖先
if(left+mid+right>=2){
res=cur;
}
//p q可能均出现在左子树或右子树
return left+mid+right>0;
}
}
49、不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
方法一,向右m-1步+向下n-1步,只要选中其中m-1步向右走的时机,剩下的n-1也就可以确定。 C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n−2m−1
方法二,动态规划。以dp[i] [j]表示走到[i] [j]的路径数量,回退一步,即等于走到[i-1] [j]和[i] [j-1]的路径数量和。首先需要初始化,当i1或者j1时,路径只有一条。T(N)=O(N2),S(N)=O(N2)。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
//只有一行或者一列时,只有一条路径
for(int i=0;i<n;++i){
dp[0][i]=1;
}
for(int j=0;j<m;++j){
dp[j][0]=1;
}
//dp[i][j]=dp[i-1][j]+dp[i][j-1]
for(int i=1;i<m;++i){
for(int j=1;j<n;++j){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
50、每日温度
根据每日 气温 列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
方法一,暴力法。找出从1到n每个位置,之后第一个比他大的数字距离当前位置的距离。T(N)=O(N^2),S(N)=O(N)。
class Solution {
public int[] dailyTemperatures(int[] T) {
int[] res=new int[T.length];
for(int i=0;i<T.length-1;++i){
int temp=T[i];
for(int j=i+1;j<T.length;++j){
if(T[j]>temp){
res[i]=j-i;
break;
}
}
}
return res;
}
}
方法二,使用栈来存储所有下标。遍历温度数组,当栈不空且栈顶对应温度小于当前温度时,说明栈顶那一天之后的升温日期已找到,栈顶出栈,更新res数组。因为当前温度可能是之前很多天的升温日期,所以应该循环搜索栈顶,可能会有多次出栈操作。T(N)=O(N),S(N)=O(N)。
这尼玛要是不写栈的标签,谁能想到这个还能用栈来解决啊。
class Solution {
public int[] dailyTemperatures(int[] T) {
Stack<Integer> stack=new Stack<>();
int[] res=new int[T.length];
for(int i=0;i<T.length;++i){
//栈顶之后第一次升高日期找到,出栈并更新,注意先保存栈顶
//每次只能更新栈顶对应位置,应该是while而非if
while(!stack.isEmpty() && T[i]>T[stack.peek()]){
int t=stack.peek();
res[t]=i-stack.pop();
}
stack.push(i);
}
return res;
}
}
51、打家劫舍III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
方法一,暴力递归。
1个爷爷有2个儿子4个孙子,因为偷取的房子不能相连,所以只能是爷爷和孙子偷,或者是儿子偷,选取较大者即可。
效率很低,887ms,强烈不推荐。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int rob(TreeNode root) {
if(root==null){
return 0;
}
//爷爷加上孙子偷得钱
int money=root.val;
if(root.left!=null){
money+=rob(root.left.left);
money+=rob(root.left.right);
}
if(root.right!=null){
money+=rob(root.right.left);
money+=rob(root.right.right);
}
//与儿子偷得钱作比较
return Math.max(money,rob(root.left)+rob(root.right));
}
}
方法二,对方法一优化,使用HashMap<TreeNode,Integer>来存储节点和能偷得钱,提高算法效率。运行5ms,还不错。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private HashMap<TreeNode,Integer> map=new HashMap<>();
public int rob(TreeNode root) {
return helper(root,map);
}
public int helper(TreeNode root,HashMap<TreeNode,Integer> map){
//递归出口
if(root==null){
return 0;
}
if(map.containsKey(root)){
return map.get(root);
}
//比较爷爷和孙子偷得钱,以及儿子偷得钱那个多
int money=root.val;
if(root.left!=null){
money+=helper(root.left.left,map);
money+=helper(root.left.right,map);
}
if(root.right!=null){
money+=helper(root.right.left,map);
money+=helper(root.right.right,map);
}
int result=Math.max(money,helper(root.left,map)+helper(root.right,map));
//存储节点
map.put(root,result);
return result;
}
}
方法三,动态规划。1ms,惊了。
每个节点有2种状态,0表示不偷,此时左右孩子拿出能偷到最多的钱即可,偷不偷不做限制;1表示偷,此时左右孩子不能偷,即均选择0状态。
我们使用一个大小为2的数组来表示 int[] res = new int[2] 0代表不偷,1代表偷
任何一个节点能偷到的最大钱的状态可以定义为
- 当前节点选择不偷: 当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
- 当前节点选择偷: 当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] result=helper(root);
return Math.max(result[0],result[1]);
}
//0代表当前节点不偷,1代表当前节点偷
public int[] helper(TreeNode root){
if(root==null){
return new int[2];
}
int[] result=new int[2];
int[] left=helper(root.left);
int[] right=helper(root.right);
//当前节点不偷,2个儿子选择最多能偷到的钱,偷不偷无所谓
result[0]=Math.max(left[0],left[1])
+Math.max(right[0],right[1]);
//当前节点偷,2个儿子绝对不能偷
result[1]=root.val+left[0]+right[0];
return result;
}
}
52、颜色分类(三色问题)
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
进阶:
一个直观的解决方案是使用计数排序的两趟扫描算法。
首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
你能想出一个仅使用常数空间的一趟扫描算法吗?
方法一,计数排序,两趟扫描,T(N)=O(N),S(N)=O(1).
方法二,Arrays.sort,T(N)=O(NlogN),S(N)=O(1)。
方法三,一趟扫描,荷兰国旗问题。
制定3个指针left、curr、right,使用curr遍历元素,遍历过程中确保nums[idx<left]均为0,nums[idx>right]均为2,当curr<=right即未遍历完成时。
- 若nums[curr]==0,将curr和left指向元素交换,因为curr左边均为扫描过得元素,交换后nums[curr]==1,所以再left++ curr++
- 若nums[curr]==2,将curr和right指向元素交换,因为交换后nums[curr]元素位置,所以right–,curr不变
- 若nums[curr]==1,curr++
T(N)=O(N),S(N)=O(1).
class Solution {
public void sortColors(int[] nums) {
//移动过程中确保nums[idx<left]均为0
//nums[idx>right]均为2
//curr遍历全部元素,知道超过right
int left=0;
int right=nums.length-1;
int curr=0;
int temp=-1;
while(curr<=right){
//curr与left交换
//curr左边都被扫描过,交换后确定nums[curr]==1,curr自增继续扫描
if(nums[curr]==0){
temp=nums[curr];
nums[curr++]=nums[left];
nums[left++]=temp;
}
//curr与right交换
//交换后curr为原right指向,nums[curr]不一定为1,所以需要继续比较,curr不能自增
else if(nums[curr]==2){
temp=nums[curr];
nums[curr]=nums[right];
nums[right--]=temp;
}
//否则继续
else if(nums[curr]==1){
curr++;
}
}
}
}
53、完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
方法一,动态规划。对于4来说,最极端情况就是1+1+1+1,dp[4]=4。然后从2开始尝试,4=2 * 2,dp[4]=Math.min(dp[4],dp[4-2 * 2]+1)=1
class Solution {
public int numSquares(int n) {
int[] dp=new int[n+1];
for(int i=1;i<=n;++i){
dp[i]=i;
for(int j=2;i-j*j>=0;++j){
dp[i]=Math.min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
}
54、除法求值(未做)
给出方程式 A / B = k, 其中 A 和 B 均为代表字符串的变量, k 是一个浮点型数字。根据已知方程式求解问题,并返回计算结果。如果结果不存在,则返回 -1.0。
示例 :
给定 a / b = 2.0, b / c = 3.0
问题: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
返回 [6.0, 0.5, -1.0, 1.0, -1.0 ]
输入为: vector<pair<string, string>> equations, vector<double>& values, vector<pair<string, string>> queries(方程式,方程式结果,问题方程式), 其中 equations.size() == values.size(),即方程式的长度与方程式结果长度相等(程式与结果一一对应),并且结果值均为正数。以上为方程式的描述。 返回vector<double>类型。
基于上述例子,输入如下:
equations(方程式) = [ ["a", "b"], ["b", "c"] ],
values(方程式结果) = [2.0, 3.0],
queries(问题方程式) = [ ["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"] ].
输入总是有效的。你可以假设除法运算中不会出现除数为0的情况,且不存在任何矛盾的结果。
55、电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
方法,回溯
回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。
给出如下回溯函数 backtrack(combination, next_digits) ,它将一个目前已经产生的组合 combination 和接下来准备要输入的数字 next_digits 作为参数。
如果没有更多的数字需要被输入,那意味着当前的组合已经产生好了。
如果还有数字需要被输入:
遍历下一个数字所对应的所有映射的字母。
将当前的字母添加到组合最后,也就是 combination = combination + letter 。
重复这个过程,输入剩下的数字: backtrack(combination + letter, next_digits[1:]) .
时间复杂度: O ( 3 N × 4 M ) O(3^N \times 4^M) O(3N×4M),其中 N 是输入数字中对应 3 个字母的数目(比方说 2,3,4,5,6,8), M 是输入数字中对应 4 个字母的数目(比方说 7,9),N+M 是输入数字的总数。
空间复杂度: O ( 3 N × 4 M ) O(3^N \times 4^M) O(3N×4M),这是因为需要保存$ 3^N \times 4^M$ 个结果。
class Solution {
private HashMap<String,String> map=new HashMap<String,String>(){{
put("2","abc");
put("3","def");
put("4","ghi");
put("5","jkl");
put("6","mno");
put("7","pqrs");
put("8","tuv");
put("9","wxyz");
}
};
private List<String> output=new ArrayList<>();
public void backtrack(String combination,String next_dighits){
//找到一种方案,加入
if(next_dighits.length()==0){
output.add(combination);
}
else{
//找到下一位数字对应的字母,遍历
String digit=next_dighits.substring(0,1);
String letters=map.get(digit);
for(int i=0;i<letters.length();++i){
String letter=map.get(digit).substring(i,i+1);
//继续向后尝试
backtrack(combination+letter,next_dighits.substring(1));
}
}
}
public List<String> letterCombinations(String digits) {
if(digits.length()!=0){
backtrack("",digits);
}
return output;
}
}
56、最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
第 1 步:状态定义
dp[i][j] 表示 [0, i] 区间内,到第 i 天(从 0 开始)状态为 j 时的最大收益。
这里 j 取三个值:
0 表示不持股;
1 表示持股;
2 表示处在冷冻期。
第 2 步:状态转移方程
不持股可以由这两个状态转换而来:(1)昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,今天卖了一股。
持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)昨天处在冷冻期,今天买了一股;
处在冷冻期只可以由不持股转换而来,因为题目中说,刚刚把股票卖了,需要冷冻 1 天。
上面的分析可以用下面这张图表示:
与之前股票问题的不同之处只在于:
从不持股状态不能直接到持股状态,得经过一个冷冻期,才能到持股状态。
第 3 步:思考初始化
在第 0 天,不持股的初始化值为 0,持股的初始化值为 -prices[0](表示购买了一股),虽然不处于冷冻期,但是初始化的值可以为 0。
第 4 步:思考输出
每一天都由前面几天的状态转换而来,最优值在最后一天。取不持股和冷冻期的最大者。
T(N)=O(N),S(N)=O(N)。
class Solution {
public int maxProfit(int[] prices) {
int length=prices.length;
if(length<2){
return 0;
}
//0 不持股(当天卖出) 1 持股 2 冷冻期
int[][] dp=new int[length][3];
dp[0][0]=0;
dp[0][1]=-prices[0];
dp[0][2]=0;
for(int i=1;i<length;++i){
//0不持股可以由前一天不持股,或者前一天持股今天卖出
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
//1持股可以由前一天冷冻期今天买入,或者前一天就已经持股
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][2]-prices[i]);
//2冷冻期只能由昨天持股卖出而来
dp[i][2]=dp[i-1][0];
}
return Math.max(dp[length-1][0],dp[length-1][2]);
}
}
57、课程表
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。
方法一,入度表,BFS。
-
统计课程安排图中每个节点的入度,生成 入度表 indegrees。
-
借助一个队列 queue,将所有入度为 0的节点入队。
-
当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
-
并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 -1,即 indegrees[cur] -= 1。
-
当入度 -1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
-
-
在每次 pre 出队时,执行 numCourses–;
- 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
- 因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
-
复杂度分析:
时间复杂度 O(N + M),遍历一个图需要访问所有节点和所有临边,N和 M 分别为节点数量和临边数量;
空间复杂度 O(N),为建立入度表所需额外空间。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//入度表
int[] inDegree=new int[numCourses];
for(int[] one:prerequisites){
inDegree[one[0]]++;
}
//队列存储入度为0的节点
Queue<Integer> queue=new LinkedList<>();
for(int i=0;i<numCourses;++i){
if(inDegree[i]==0){
queue.offer(i);
}
}
//循环出队入度为0的节点
while(!queue.isEmpty()){
Integer pre=queue.remove();
numCourses--;
for(int[] one:prerequisites){
//从pre指向其他节点,并不是,继续遍历
if(one[1]!=pre){
continue;
}
//从pre指向其他节点,是的,指向的节点入度-1
if(--inDegree[one[0]]==0){
//减到0之后,入队
queue.offer(one[0]);
}
}
}
return numCourses==0;
}
}
方法二,DFS,深度遍历。
-
借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
-
未被 DFS 访问:i == 0;
-
已被其他节点启动的DFS访问:i == -1;
-
已被当前节点启动的DFS访问:i == 1。
-
-
对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
- 终止条件:
当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环,直接返回 False。 - 将当前访问节点 i 对应 flag[i] 置 11,即标记其被本轮 DFS 访问过;
- 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
- 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1 并返回 True。
- 终止条件:
-
若整个图 DFS 结束并未发现环,返回 True。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//邻接矩阵
int[][] matrix=new int[numCourses][numCourses];
//flag状态
int[] flags=new int[numCourses];
for(int[] one:prerequisites){
matrix[one[1]][one[0]]=1;
}
for(int i=0;i<numCourses;++i){
if(!dfs(matrix,flags,i)){
return false;
}
}
return true;
}
public boolean dfs(int[][] matrix,int[] flags, int i){
//已经被其他节点遍历到,不存在环
if(flags[i]==-1){
return true;
}
//已经被当前节点遍历,存在环,无法拓扑排序
if(flags[i]==1){
return false;
}
flags[i]=1;
for(int j=0;j<matrix.length;++j){
//存在环
if(matrix[i][j]==1 && !dfs(matrix,flags,j)){
return false;
}
}
flags[i]=-1;
return true;
}
}
58、字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例:
s = "3[a]2[bc]", 返回 "aaabcbc".
s = "3[a2[c]]", 返回 "accaccacc".
s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef".
剑指Offer简单难度
1、左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
限制:
1 <= k < s.length <= 10000
解题思路:先翻转前半部分,再翻转后半部分,然后全部翻转,但是这样很耗时,还不如直接String.substring然后拼接…
class Solution {
public String reverseLeftWords(String s, int n) {
//解法2
//StringBuffer sb=new StringBuffer();
//return sb.append(s.substring(n))
// .append(s.substring(0,n)).toString();
char[] str=s.toCharArray();
//先翻转前半部分,再翻转后半部分,然后全部翻转
reverse(str,0,n-1);
reverse(str,n,s.length()-1);
reverse(str,0,s.length()-1);
return String.valueOf(str);
}
public void reverse(char[] str,int begin,int end){
//i=begin对应end,之后每次前进后退1,i对应end-(i-begin)=end+begin-i
for(int i=begin;i<(end+begin+1)/2;++i){
char temp=str[i];
str[i]=str[end+begin-i];
str[end+begin-i]=temp;
}
return;
}
}
2、二叉树的深度
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
解题思路:Queue进行层级遍历,或者直接递归。递归效率更高…
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
//return 1+Math.max(maxDepth(root.left),maxDepth(root.right));
int count=0;
Queue<TreeNode> queue=new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
count++;
int curSize=queue.size();
for(int i=0;i<curSize;++i){
TreeNode node=queue.poll();
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
return count;
}
}
3、链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解题思路:快慢指针,快指针先走k步,然后同步前进,当快指针指向null时,slow所指即为倒数第k个节点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow,fast;
slow=head;fast=head;
while(k!=0){
fast=fast.next;
--k;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
}
4、打印从1到最大的n位数
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
示例 1:
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
说明:
用返回一个整数列表来代替打印
n 为正整数
class Solution {
public int[] printNumbers(int n) {
int[] bound={ 10, 100, 1000, 10_000, 100_000, 1_000_000, 10_000_000,
100_000_000, 1_000_000_000, Integer.MAX_VALUE };
int[] nums=new int[bound[n-1]-1];
for(int i=0;i<bound[n-1]-1;++i){
nums[i]=i+1;
}
return nums;
}
}
原有题目解法:使用回溯来实现0-9的全排列,防止大数溢出问题,打印数字。
class Solution{
public void helper(int n){
if(n<=0){
return;
}
char[] num=new char[n];
helperCore(0,n,num);
}
public void helperCore(int index,int length,char[] num){
//凑够n位
if(index==length){
printNum(num);
return;
}
for(char i='0';i<='9';++i){
num[index]=i;
helperCore(index+1,n,num);
}
}
public printNum(char[] num){
int index=0;
//找到第一位不为0的字符
for(;index<num.length;++index){
if(num[index]!='0'){
break;
}
}
for(;index<nums.length;++index){
System.out.print(num[index]);
}
System.out.println();
}
}
5、二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
限制:
0 <= 节点个数 <= 1000
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root==null){
return null;
}
if(root.left!=null && root.right!=null){
TreeNode node=root.left;
root.left=root.right;
root.right=node;
}else if(root.left!=null && root.right==null){
root.right=root.left;
root.left=null;
}else if(root.left==null && root.right!=null){
root.left=root.right;
root.right=null;
}
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}
}
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func mirrorTree(root *TreeNode) *TreeNode {
// 解法1:递归
// // 无左右子树
// if root==nil || (root.Left==nil &&root.Right==nil){
// return root
// }
// // 左右子树都有 交换 递归
// temp:=root.Left
// root.Left=root.Right
// root.Right=temp
// mirrorTree(root.Left)
// mirrorTree(root.Right)
// return root
// 解法2:广度优先使用slice实现
if root==nil || (root.Left==nil &&root.Right==nil){
return root
}
stack:=make([]*TreeNode,0)
stack = append(stack,root)
for(len(stack)>0) {
node:=stack[0]
stack=stack[1:]
// 进入
if node.Left!=nil{
stack = append(stack,node.Left)
}
if node.Right!=nil{
stack = append(stack,node.Right)
}
// 交换
node.Left,node.Right=node.Right,node.Left
}
return root
}
6、替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
限制:
0 <= s 的长度 <= 10000
解题思路:使用StringBuffer遍历原String.toCharArray()并同时进行操作。
当然还有更高效的原地置换算法,只不过Java不适用。
class Solution {
public String replaceSpace(String s) {
StringBuffer sb=new StringBuffer();
char[] str=s.toCharArray();
for(char ch:str){
if(ch==' '){
sb.append("%20");
}
else{
sb.append(ch);
}
}
return sb.toString();
}
}
7、从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
限制:
0 <= 链表长度 <= 10000
解题思路:使用Stack实现倒序打印,或者使用额外指针先倒序再打印。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
if(head==null){
return new int[0];
}
ListNode pre=null,cur=head,nex=head.next;
int size=0;
//翻转链表
while(cur!=null){
cur.next=pre;
pre=cur;
cur=nex;
nex=nex==null?null:nex.next;
size++;
}
//逐个打印
int[] res=new int[size];
size=0;
while(pre!=null){
res[size]=pre.val;
pre=pre.next;
size++;
}
return res;
}
}
class Solution {
public int[] reversePrint(ListNode head) {
Stack<ListNode> stack = new Stack<ListNode>();
ListNode temp = head;
while (temp != null) {
stack.push(temp);
temp = temp.next;
}
int size = stack.size();
int[] res = new int[size];
for (int i = 0; i < size; i++) {
res[i] = stack.pop().val;
}
return res;
}
}
8、反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:
0 <= 节点个数 <= 5000
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null){
return head;
}
ListNode pre=null,cur=head,nex=head.next;
while(cur!=null){
cur.next=pre;
pre=cur;
cur=nex;
nex=nex==null?null:nex.next;
}
return pre;
}
}
9、合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
限制:
0 <= 链表长度 <= 1000
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
//伪头结点方便最后返回
ListNode res=new ListNode(0),head=res;
//一者不为空,把较小者拼接到后面
while(l1!=null && l2!=null){
if(l1.val<l2.val){
head.next=l1;
l1=l1.next;
}
else{
head.next=l2;
l2=l2.next;
}
head=head.next;;
}
//如有剩余继续拼接
head.next=l1!=null?l1:l2;
return res.next;
}
}
10、用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例 1:
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:
输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
提示:
1 <= values <= 10000
最多会对 appendTail、deleteHead 进行 10000 次调用
解题思路:主栈one与辅助栈other,one正常入栈,出栈时,先将one出栈并加入other栈,然后other出栈即可。
class CQueue {
private Stack<Integer> one;
private Stack<Integer> other;
public CQueue() {
super();
one=new Stack<>();
other=new Stack<>();
}
public void appendTail(int value) {
one.push(value);
return;
}
public int deleteHead() {
int res;
//辅助栈为空
if(other.isEmpty()){
if(one.isEmpty()){
res=-1;
}
else{//主栈不空,出栈加入辅助栈
while(!one.isEmpty()){
other.push(one.pop());
}
res=other.pop();
}
}//辅助栈不为空,直接栈顶出栈
else{
res=other.pop();
}
return res;
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
11、二进制中1的个数
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res=0;
while(n!=0){
res+=n&1;
//一定要采用无符号右移,否则负数会OOM
n>>>=1;
}
return res;
}
}
12、二叉搜索树的第k大节点
给定一棵二叉搜索树,请找出其中第k大的节点。
示例 1:
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 4
示例 2:
输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/ \
3 6
/ \
2 4
/
1
输出: 4
限制:
1 ≤ k ≤ 二叉搜索树元素个数
解题思路:利用二叉搜索树的性质,先遍历右子树,再是根节点,如果count==1就是根节点的值,否则继续遍历左子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private int count;
private int result=-1;
public int kthLargest(TreeNode root, int k) {
this.count=k;
helper(root);
return result;
}
public void helper(TreeNode root){
if(root!=null){
helper(root.right);
if(count==1){
result=root.val;
count--;
return;
}
count--;
helper(root.left);
}
}
}
13、从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
提示:
节点总数 <= 1000
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if(root==null){
return res;
}
Queue<TreeNode> queue=new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
List<Integer> list=new ArrayList<>();
int size=queue.size();
for(int i=0;i<size;++i){
TreeNode node=queue.poll();
list.add(node.val);
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
res.add(list);
}
return res;
}
}
14、和为s的连续正数序列
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例 1:
输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
限制:
1 <= target <= 10^5
解题思路:梯形面积求和,left=1,right=target/2+1,每次的连续和逐渐向中间逼近。但是这种思路在只能循环一次,而且一定是第一次从1开始,第一个9直接就找不出来了。改用滑动窗口,left=1,right=2,计算连续和temp,temp<target,right右移,把temp变大一点;temp>target,left左移,把temp变小一点;temp==target,将int[]加入res,然后right++使得循环继续。循环的结束条件,是right<=target/2+1。
class Solution {
public int[][] findContinuousSequence(int target) {
int left=1,right=2;
List<int[]> res=new ArrayList<>();
while(right<=target/2+1){
int temp=(right-left+1)*(right+left)/2;
if(temp<target){
right++;
}
else if(temp>target){
left++;
}else{
int[] tempList=new int[right-left+1];
for(int i=left;i<=right;++i){
tempList[i-left]=i;
}
right++;
res.add(tempList);
}
}
return res.toArray(new int[0][]);
}
}
15、叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。
解题思路:利用好二叉搜索树的性质,左边都比根节点小,右边都比根节点大。当pq都大于root,继续到右子树寻找;pq都小于root,继续到左子树寻找;否则就是root排在中间,即root为最近公共祖先。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root!=null){
if(root.val<p.val && root.val<q.val){
root=root.right;
}
else if(root.val>p.val && root.val>q.val){
root=root.left;
}
else{
return root;
}
}
return root;
}
}
16、数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:
2 <= n <= 100000
解题思路:如果没有重复数字,那么正常排序后,数字i应该在下标为i的位置,所以思路是重头扫描数组,遇到下标为i的数字如果不是i的话,(假设为m),那么我们就拿与下标m的数字交换。在交换过程中,如果有重复的数字发生,那么终止返回ture。
class Solution {
public int findRepeatNumber(int[] nums) {
for(int i=0;i<nums.length;++i){
while(i!=nums[i]){
//找到重复,跳出
if(nums[i]==nums[nums[i]]){
return nums[i];
}
//暂不重复,nums[i]归位,循环结束
int temp=nums[i];
nums[i]=nums[temp];
nums[temp]=temp;
}
}
return -1;
}
}
17、数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
限制:
1 <= 数组长度 <= 50000
解题思路:
算法原理:
为构建正负抵消,假设数组首个元素 n_1 为众数,遍历统计票数,当发生正负抵消时,剩余数组的众数一定不变 ,这是因为:
当$ n_1 = x $: 抵消的所有数字中,有一半是众数。
当
n
1
≠
x
n_1 \neq x
n1=x : 抵消的所有数字中,少于或等于一半是众数。
利用此特性,每轮假设都可以 缩小剩余数组区间 。当遍历完成时,最后一轮假设的数字即为众数(由于众数超过一半,最后一轮的票数和必为正数)。
算法流程:
初始化: 票数统计 votes=0 , 众数 x;
循环抵消: 遍历数组 nums 中的每个数字 num ;
当 票数 votes 等于 0 ,则假设 当前数字num 为 众数 xx ;
当 num = x 时,票数 votes 自增 1 ;否则,票数votes 自减 11 。
返回值: 返回 众数 x 即可。
或者直接Arrays.sort排序返回中间值即可。
class Solution {
public int majorityElement(int[] nums) {
int votes=0;
int res=-1;
for(int num:nums){
if(votes==0){
res=num;
}
votes+=num==res?1:-1;
}
return res;
}
}
18、二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
解题思路:先递归左子树,再递归右子树,一者为空则返回另一个。如果都为空,说明pq分别在两边,返回root。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null || root==p || root==q){
return root;
}
TreeNode leftNode=lowestCommonAncestor(root.left,p,q);
TreeNode rightNode=lowestCommonAncestor(root.right,p,q);
if(leftNode==null){
return rightNode;
}
if(rightNode==null){
return leftNode;
}
return root;
}
}
19、和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:
输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
限制:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^6
解题思路:两个指针向中间移动。
class Solution {
public int[] twoSum(int[] nums, int target) {
int leftIndex=0,rightIndex=nums.length-1;
int[] res=new int[2];
while(leftIndex<rightIndex){
int temp=nums[leftIndex]+nums[rightIndex];
if(temp<target){
leftIndex++;
}else if(temp>target){
rightIndex--;
}
else{
res[0]=nums[leftIndex];
res[1]=nums[rightIndex];
break;
}
}
return res;
}
}
20、调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
示例:
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
提示:
1 <= nums.length <= 50000
1 <= nums[i] <= 10000
解题思路:两个指针向中间靠近,注意寻找两个指针位置时的特殊情况即可。
class Solution {
public int[] exchange(int[] nums) {
int leftDoub=0,rightOdd=nums.length-1;
while(leftDoub<rightOdd){
//找到偶数
while(nums[leftDoub]%2==1 && leftDoub<rightOdd){
leftDoub++;
}
//找到奇数
while(nums[rightOdd]%2==0 && leftDoub<rightOdd){
rightOdd--;
}
if(leftDoub<rightOdd){
int temp=nums[leftDoub];
nums[leftDoub]=nums[rightOdd];
nums[rightOdd]=temp;
leftDoub++;
rightOdd--;
}
}
return nums;
}
}
21、相交链表,见热题100简单难度第14题
22、包含min函数的栈,见热题100简单难度第15题
23、删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
注意:此题对比原题有改动
示例 1:
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
题目保证链表中节点的值互不相同
若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
解题思路:遍历一遍链表。原题是给出了要删除的节点,对于头结点,直接返回下一个节点;对于非头非尾节点,将下一个节点值覆盖此节点,然后删除下一个节点即可;对于尾节点,需要遍历找到前一个节点并断开尾节点。T(N)=(O(1)* N+O(N-1))/N=O(1)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
ListNode node=new ListNode(0);
node.next=head;
head=node;
while(head.next!=null){
if(head.next.val==val){
head.next=head.next.next;
}
head=head.next;
}
return node.next;
}
}
class Solution {
public ListNode deleteNode(ListNode head, ListNode val) {
if(head==null || val==null){
return null;
}
//非尾节点
if(val.next!=null){
ListNode next=val.next;
val.val=next.val;
val.next=next.next;
}
//只有一个节点,且删除
else if(head==val){
head==null;
}
//尾节点
else{
ListNode cur=head;
while(cur.next!=val){
cur=cur.next;
}
cur.next=null;
}
return head;
}
}
24、平衡二叉树
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
1
/ \
2 2
/ \
3 3
/ \
4 4
返回 false 。
限制:
1 <= 树的结点个数 <= 10000
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if(root==null){
return true;
}
int leftDepth=depth(root.left);
int rightDepth=depth(root.right);
if(Math.abs(leftDepth-rightDepth)<2){
return isBalanced(root.left) && isBalanced(root.right);
}
return false;
}
public int depth(TreeNode root){
if(root==null){
return 0;
}
return 1+Math.max(depth(root.left),depth(root.right));
}
}
25、 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
限制:
0 <= s 的长度 <= 50000
解题思路:遍历一次char[]建立HashMap,再次遍历获取第一个计数为1的char。
class Solution {
public char firstUniqChar(String s) {
Map<Character,Integer> map=new LinkedHashMap<>();
char[] chs=s.toCharArray();
char res=' ';
//建立HashMap
for(char ch:chs){
if(map.containsKey(ch)){
map.put(ch,map.get(ch)+1);
}else{
map.put(ch,1);
}
}
//找到第一个出现一次的字符
for(char ch:chs){
if(map.get(ch)==1){
res=ch;
break;
}
}
return res;
}
}
26、连续子数组的最大和,与热题100简单难度17最大子序和相同
27、对称的二叉树,与热题100简单难度16题相同
28、构建乘积数组,与热题100中等难度34题相同
29、最小的k个数
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
解题思路:使用大顶堆即Java的PriorityQueue降序排列,保留数组k的数字,当超出k个且堆顶最大元素小于num时,替换,最后剩下的就是数组最小的k个数。
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if(k==0){
return new int[0];
}
//ab满足b-a<0,即降序排列,大顶堆
Queue<Integer> heap=new PriorityQueue<>((a,b)->(b-a));
for(int num:arr){
if(heap.size()<k){
heap.offer(num);
}else{//num比堆顶小时,堆顶出栈
if(heap.peek()>num){
heap.poll();
heap.offer(num);
}
}
}
int[] res=new int[k];
for(k=k-1;k>-1;k--){
res[k]=heap.poll();
}
return res;
}
}
30、不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
示例:
输入: a = 1, b = 1
输出: 2
提示:
a, b 均可能是负数或 0
结果不会溢出 32 位整数
解题思路:不用加减乘除做加法的方法是使用按位异或和按位与运算。计算a + b等价于计算(a ^ b) + ((a & b) << 1),其中((a & b) << 1)表示进位。因此令a等于(a & b) << 1,令b等于a ^ b,直到a变成0,即不再需要进位时,不进位的结果(^操作)就表示最后结果,然后返回b。
class Solution{
public int add(int a,int b){
while(a!=0){//无需进位时停止
int temp=a^b;//不进位结果
a=(a&b)<<1;//进位结果
b=temp;
}
}
}
31、n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
限制:
1 <= n <= 11
解题思路:动态规划。
1、表示状态。通过题目我们知道一共投掷 n 枚骰子,那最后一个阶段很显然就是:当投掷完 n 枚骰子后,各个点数出现的次数。
注意,这里的点数指的是前 n 枚骰子的点数和,而不是第 n 枚骰子的点数,下文同理。
找出了最后一个阶段,那状态表示就简单了。
首先用数组的第一维来表示阶段,也就是投掷完了几枚骰子。
然后用第二维来表示投掷完这些骰子后,可能出现的点数。
数组的值就表示,该阶段各个点数出现的次数。
所以状态表示就是这样的:dp[i] [j],表示投掷完 i 枚骰子后,点数 j 的出现次数。
2、找出状态转移方程。找状态转移方程也就是找各个阶段之间的转化关系,同样我们还是只需分析最后一个阶段,分析它的状态是如何得到的。
最后一个阶段也就是投掷完 n 枚骰子后的这个阶段,我们用 dp[n] [j]来表示最后一个阶段点数 jj 出现的次数。
单单看第 n 枚骰子,它的点数可能为 1 , 2, 3, … , 6,因此投掷完 n 枚骰子后点数 jj 出现的次数,可以由投掷完 n−1 枚骰子后,对应点数 j-1, j-2, j-3, … , j-6 出现的次数之和转化过来。
for (第n枚骰子的点数 i = 1; i <= 6; i ++) {
dp[n] [j] += dp[n-1] [j - i]
}
写成数学公式是这样的:
d p [ n ] [ j ] = ∑ i = 1 6 d p [ n − 1 ] [ j − i ] dp[n][j] = \sum_{i=1}^6 dp[n-1][j-i] dp[n][j]=∑i=16dp[n−1][j−i]
n 表示阶段,j表示投掷完 n 枚骰子后的点数和,i 表示第 n 枚骰子会出现的六个点数。
3、边界处理。dp[1] [1-6]都初始化为1即可。
class Solution {
public double[] twoSum(int n) {
double[] res=new double[n*6-n+1];
int[][] dp=new int[15][70];
for(int i=1;i<7;++i){
dp[1][i]=1;
}
for(int i=2;i<=n;++i){
for(int j=i;j<=6*i;++j){
for(int cur=1;cur<=6;++cur){
if(j-cur<=0){
break;
}
dp[i][j]+=dp[i-1][j-cur];
}
}
}
double all=Math.pow(6,n);
for(int i=0;i<res.length;++i){
res[i]=(dp[n][n+i]*1.0)/all;
}
return res;
}
}
32、圆圈中最后剩下的数字
0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例 1:
输入: n = 5, m = 3
输出: 3
示例 2:
输入: n = 10, m = 17
输出: 2
限制:
1 <= n <= 10^5
1 <= m <= 10^6
解题思路:n个数字的圆圈,不断删除第m个数字,我们把最后剩下的数字记为f(n,m)
n个数字中第一个被删除的数字是(m-1)%n (取余的原因是m可能比n大), 我们记作k,k=(m-1)%n
那么剩下的n-1个数字就变成了:0,1,……k-1,k+1,……,n-1,我们把下一轮第一个数字排在最前面,并且将这个长度为n-1的数组映射到0~n-2。
原始数组 | 映射数字 |
---|---|
k+1 | 0 |
k+2 | 1 |
… | … |
n-1 | n-k-2 |
0 | n-k-1 |
… | … |
k-1 | n-2 |
把映射数字记为x,原始数字记为y,那么映射数字变回原始数字的公式为
y
=
(
x
+
k
+
1
)
m
o
d
n
y=(x+k+1) \mod n
y=(x+k+1)modn
在映射数字中,n-1个数字,不断删除第m个数字,由定义可以知道,最后剩下的数字为f(n-1,m)。我们把它变回原始数字,由上一个公式可以得到最后剩下的原始数字是(f(n-1,m)+k+1)%n,而这个数字也就是一开始我们标记的f(n,m),所以可以推得递归公式为
f
(
n
,
m
)
=
(
f
(
n
−
1
,
m
)
+
k
+
1
)
m
o
d
n
f(n,m) =(f(n-1,m)+k+1)\mod n
f(n,m)=(f(n−1,m)+k+1)modn
将k=(m-1)%n代入,化简得到:
f
(
n
,
m
)
=
(
f
(
n
−
1
,
m
)
+
m
)
m
o
d
n
,
且
f
(
1
,
m
)
=
0
f(n,m) =(f(n-1,m)+m)\mod n, 且f(1,m) = 0
f(n,m)=(f(n−1,m)+m)modn,且f(1,m)=0
代码中可以采用迭代或者递归的方法实现该递归公式。时间复杂度为O(n),空间复杂度为O(1)
注意公式中的mod就等同于%,为取模运算。值得注意的是,在数学中,下式成立:
(
a
(a%n+b)%n=(a+b)%n
(a
//迭代
public int lastRemaining(int n, int m) {
int flag = 0;
for (int i = 2; i <= n; i++) {
flag = (flag + m) % i;
//动态规划的思想,将f(n,m)替换成flag存储
}
return flag;
}
//递归
public int lastRemaining(int n, int m){
if(n < 1 || m < 1)
return -1;
if(n == 1)
return 0;
return (lastRemaining(n-1, m) + m) % n;
}
33、斐波拉契数列
34、青蛙跳台阶问题,同斐波拉契数列
35、二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
限制:
0 <= n <= 1000
0 <= m <= 1000
解题思路:将 matrix 中的左下角元素(标志数)记作 flag ,则有:
若 flag > target ,则 target 一定在 flag 所在行的上方,即 flag 所在行可被消去。
若 flag < target ,则 target 一定在 flag 所在列的右方,即 flag 所在列可被消去。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
//从右下角[i,j]开始查找
//每次淘汰一行或者一列
int i=matrix.length-1,j=0;
while(i>=0 && j<matrix[0].length){
//此行都比target大,淘汰当前行
if(matrix[i][j]>target){
i--;
}//target位于当前行,列后移
else if(matrix[i][j]<target){
j++;
}else{
return true;
}
}
return false;
}
}
36、旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
示例 1:
输入:[3,4,5,1,2]
输出:1
示例 2:
输入:[2,2,2,0,1]
输出:0
解题思路:二分法寻找旋转点,旋转之后左半部分一定都比右半部分大,再和中间节点比较一下,确定旋转点所在位置。
class Solution {
public int minArray(int[] numbers) {
int left=0,right=numbers.length-1,mid=0;
while(left<right){
mid=(left+right)/2;
if(numbers[mid]>numbers[right]){
left=mid+1;
}
else if(numbers[mid]<numbers[right]){
right=mid;
}
else{
right--;
}
}
return numbers[left];
}
}
37、顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
限制:
0 <= matrix.length <= 100
0 <= matrix[i].length <= 100
解题思路:定义4个边界left/right/top/bottom,初始化为0和行列-1。
遍历上边一行,[top] [left~right],修改top并判断是否满足循环条件;
遍历右边一行,[top~bottom] [right],修改right并判断是否满足循环条件;
遍历下边一行,[bottom] [left~right],修改bottom并判断是否满足循环条件;
遍历左边一行,[top~bottom] [left],修改left并判断是否满足循环条件;
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length==0){
return new int[0];
}
int left=0,right=matrix[0].length-1,top=0,bottom=matrix.length-1;
int x=0;
int[] res=new int[(right+1)*(bottom+1)];
while(true){
//遍历上边一行,并改变top
for(int i=left;i<=right;++i){
res[x++]=matrix[top][i];
}
if(++top>bottom){
break;
}
//遍历右边一列,并改变right
for(int i=top;i<=bottom;++i){
res[x++]=matrix[i][right];
}
if(left>--right){
break;
}
//遍历下边一行,并修改bottom
for(int i=right;i>=left;--i){
res[x++]=matrix[bottom][i];
}
if(top>--bottom){
break;
}
//遍历左边一列,并修改left
for(int i=bottom;i>=top;--i){
res[x++]=matrix[i][left];
}
if(++left>right){
break;
}
}
return res;
}
}
38、在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
限制:
0 <= 数组长度 <= 50000
解题思路:利用排序数组特点,用二分法分别取寻找左右边界。
当nums[mid]<target,说明边界在mid右边,left=mid+1;
当nums[mid]>target,说明边界在mid左边,right=mid-1;
当nums[mid]==target,说明左边界在mid左边,right=mid-1;
说明右边界在mid右边,left=mid+1;
查找左右边界代码几乎一样,只是对情况处理不同,还有一点需要注意,因为循环条件是left<=right,最后leftright时,mid==left,left=mid+1>right,left存储右边界,right存储左边界。
class Solution {
public int search(int[] nums, int target) {
int left=0,right=nums.length-1,middle=0;
//查找右边界
while(left<=right){
middle=(left+right)/2;
if(nums[middle]<=target){
left=middle+1;
}
else{
right=middle-1;
}
}
int finalRight=left;
//寻找左边界
left=0;right=nums.length-1;
while(left<=right){
middle=(left+right)/2;
if(nums[middle]<target){
left=middle+1;
}else{
right=middle-1;
}
}
int finalLeft=right;
return finalRight-finalLeft-1;
}
}
39、0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例 1:
输入: [0,1,3]
输出: 2
示例 2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
限制:
1 <= 数组长度 <= 10000
解题思路:
排序数组中的搜索问题,首先想到 二分法 解决。
根据题意,数组可以按照以下规则划分为两部分。
- 左子数组: nums[i] = i;
- 右子数组: n u m s [ i ] ≠ i nums[i] \neq i nums[i]=i ;
缺失的数字等于 “右子数组的首位元素” 对应的索引;因此考虑使用二分法查找 “右子数组的首位元素” 。
-
初始化: 左边界 i = 0,右边界 j = len(nums) - 1;代表闭区间 [i, j] 。
-
循环二分:当 i ≤ j i \leq j i≤j时循环(即当闭区间 [i, j][i,j] 为空时跳出);
- 计算中点 m = (i + j) / 2
- 若 nums[m] = m ,则 “右子数组的首位元素” 一定在闭区间 [m + 1, j] 中,因此执行 i = m + 1;
- 若 n u m s [ m ] ≠ m nums[m] \ne m nums[m]=m ,则 “左子数组的末位元素” 一定在闭区间 [i, m - 1] 中,因此执行 j = m - 1;
-
返回值: 跳出时,变量 i和 j 分别指向 “右子数组的首位元素” 和 “左子数组的末位元素” 。因此返回 i即可
class Solution {
public int missingNumber(int[] nums) {
int left=0,right=nums.length-1 ,mid=0;
while(left<=right){
mid=(left+right)/2;
if(nums[mid]==mid){
left=mid+1;
}else{
right=mid-1;
}
}
return left;
}
}
40、翻转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
示例 1:
输入: "the sky is blue"
输出: "blue is sky the"
示例 2:
输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:
输入: "a good example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
说明:
无空格字符构成一个单词。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
class Solution {
public String reverseWords(String s) {
String[] a=s.split(" ");
StringBuffer sb=new StringBuffer();
for(int i=a.length-1;i>-1;i--){
if(!"".equals(a[i])){
sb.append(a[i]);
sb.append(" ");
}
}
return sb.toString().trim();
}
}
41、滑动窗口的最大值
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
提示:
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
解题思路:大顶堆,复杂度O(NlgN),不推荐。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums==null || nums.length==0 ||k<=0){
return new int[0];
}
PriorityQueue<Integer> maxHeap=new PriorityQueue<>((a,b)->(b-a));
int i=0,j=0;
while(i<k){
maxHeap.offer(nums[i++]);
}
int[] res=new int[nums.length-k+1];
res[j++]=maxHeap.peek();
while(i<nums.length){
maxHeap.remove(nums[i-k]);
maxHeap.offer(nums[i++]);
res[j++]=maxHeap.peek();
}
return res;
}
}
42、扑克牌中的顺子
从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
示例 1:
输入: [1,2,3,4,5]
输出: True
示例 2:
输入: [0,0,1,2,5]
输出: True
限制:
数组长度为 5
数组的数取值为 [0, 13] .
解题思路:有一串连续的数字(无重复),这串数字中最大值为 m, 最小值为 n,问你这串数字中一共有多少个数字?m - n + 1
同样,如果我们能够知道 5 张扑克牌中的最大值 maxValue 和最小值 minValue ,那我们就知道,要使它为顺子需要 maxValue - minValue + 1 张牌。
-
在查找 maxValue 和 minValue 过程中,跳过大小王 0 。
-
如果maxValue - minValue + 1 > 5,说明题目给的 5 张牌不足以构成顺子,返回false
即使里面有大小王,也不够用来填补使它构成顺子。
-
如果maxValue - minValue + 1 <= 5,说明 5 张牌足以构成顺子,返回true。里面的大小王填补在合适位置即可。
同时,我们再定义一个标志数组判断是否有重复数字,发现重复数字直接返回 false即可。
class Solution {
public boolean isStraight(int[] nums) {
boolean[] repeat=new boolean[15];
int maxValue=0,minValue=14;
for(int num:nums){
//大小王不做判断
if(num==0){
continue;
}
//num已经出现过,有重复,不能构成顺子
if(repeat[num]){
return false;
}
//更新访问记录和最大最小值
repeat[num]=true;
maxValue=Math.max(maxValue,num);
minValue=Math.min(minValue,num);
}
return maxValue-minValue+1<=5;
}
}
剑指Offer中等难度
1、丑数
我们把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:
1 是丑数。
n 不超过1690。
解题思路:一定要注意啊,是只包含 2/3/5的数,不是包含2/3/5的数,坑爹啊。
这道题可以用动态规划来解,大的数字一定是前边小的数字 *2 *3 *5,但是怎么确定是选择哪个小数字呢?肯定是选择 * 2 / *3 / *5 *运算之后最小的那个。定义3个指针p2 p3 p5,从0开始,如果dp[i]==dp[p2] *2,说明0~p2指向的数字 *2不会再比dp[i]更大了,p2前进,p3 p5同理。T(N)=O(N),S(N)=O(N)。
class Solution {
public int nthUglyNumber(int n) {
//特殊情况
if(n==1){
return 1;
}
int[] dp=new int[n];
int p2=0;
int p3=0;
int p5=0;
dp[0]=1;
//后边的丑数肯定是前边的丑数*2/*3/*5
//肯定是选择最小的值,不然会漏掉
for(int i=1;i<n;i++){
dp[i]=Math.min(dp[p2]*2,Math.min(dp[p3]*3,dp[p5]*5));
//前边0~p2-1不会产生比dp[i]更大的值了,前进
if(dp[i]==dp[p2]*2){
p2++;
}
if(dp[i]==dp[p3]*3){
p3++;
}
if(dp[i]==dp[p5]*5){
p5++;
}
}
return dp[n-1];
}
}
2、从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[20,9],
[15,7]
]
提示:
节点总数 <= 1000
解题思路:队列的先进先出特性实现层次遍历。根节点入队,之后依次将队列所有元素(即本层)出队(需要先记录本层size),并将本层所有节点值压入,然后节点左右子树入队。如此循环依次即遍历完一层,遍历到队列为空就可以停止了。至于之字形遍历,使用boolean变量来控制,使用Collections.reverse()来翻转List即可。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
if(root==null){
return new ArrayList<>();
}
boolean needMirror=false;
Queue<TreeNode> queue=new LinkedList<>();
queue.add(root);
//队列不空时循环
while(!queue.isEmpty()){
//当前层全部出队
int size=queue.size();
List<Integer> curFloor=new ArrayList<>();
for(int i=0;i<size;++i){
TreeNode tempNode=queue.poll();
curFloor.add(tempNode.val);
//左右子树入队
if(tempNode.left!=null){
queue.add(tempNode.left);
}
if(tempNode.right!=null){
queue.add(tempNode.right);
}
}
//需要逆序则进行逆序
if(needMirror){
Collections.reverse(curFloor);
}
needMirror=!needMirror;
res.add(curFloor);
}
return res;
}
}
3、二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
解题思路:使用分治思路。最重要的一步就是知道首尾节点first/last,使用helper函数将搜索树转为链表,然后将整体的fisrt/last进行连接。在helper函数中,首先分别对左右子树进行helper展开为双向链表,然后将当前节点复制一份(防止原二叉树混乱),然后分别针对左右双向链表为空不为空的情况,确定整体的first/last并将左右双向链表与当前复制节点进行双向连接。T(N)=O(N),S(N)=O(N).
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val,Node _left,Node _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
public Node first,last;
//确保测试用例通过
public Solution(){
super();
}
public Solution(Node first,Node last){
this.first=first;
this.last=last;
}
public Node treeToDoublyList(Node root) {
//root为空
if(root==null){
return null;
}
//将分治后双向链表first/last连接
Solution sol=helper(root);
sol.first.left=sol.last;
sol.last.right=sol.first;
return sol.first;
}
public Solution helper(Node root){
//走到叶子下
if(root==null){
return null;
}
//左右子树分别展开
Solution leftDoub=helper(root.left);
Solution rightDoub=helper(root.right);
//复制当前节点,防止原二叉树混乱
Node curNode=new Node(root.val);
Solution sol=new Solution(null,null);
//左双向链表与当前节点连接,并确定整体first
if(leftDoub==null){
sol.first=curNode;
}else{
sol.first=leftDoub.first;
leftDoub.last.right=curNode;
curNode.left=leftDoub.last;
}
//右双向链表与当前节点连接,并确定整体right
if(rightDoub==null){
sol.last=curNode;
}else{
sol.last=rightDoub.last;
curNode.right=rightDoub.first;
rightDoub.first.left=curNode;
}
return sol;
}
}
4、栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
提示:
0 <= pushed.length == popped.length <= 1000
0 <= pushed[i], popped[i] < 1000
pushed 是 popped 的排列。
解题思路:使用stack模拟入栈操作,每次入栈后,只要栈不空且popped未遍历结束且popped当前元素与栈顶元素相等,就出栈并且继续遍历下一个元素。
T(N)=O(N),S(N)=O(N).
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack=new Stack<>();
int poppedIndex=0;
int length=pushed.length;
//入栈后循环找到可以出栈的元素
for(int i=0;i<length;++i){
stack.push(pushed[i]);
while(!stack.isEmpty() && poppedIndex<length
&& stack.peek()==popped[poppedIndex]){
stack.pop();
++poppedIndex;
}
}
return stack.isEmpty();
}
}
5、把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
提示:
0 < nums.length <= 100
说明:
输出结果可能非常大,所以你需要返回一个字符串而不是整数
拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0
解题思路:因为输出结果可能会非常大,所以先将int[]转换成List< String>。然后自定义排序,排序之后排列的s1=10 s2=2,应该是s1+s2=102<s2+s1=210,使用Lambda形式实现Comparator自定义比较器。T(N)=O(N),S(N)=O(N).
class Solution {
public String minNumber(int[] nums) {
List<String> strList=new ArrayList<>();
for(int num:nums){
strList.add(String.valueOf(num));
}
//排序后s1 s2应为s1+s2<s2+s1
strList.sort((s1,s2)->(s1+s2).compareTo(s2+s1));
StringBuffer sb=new StringBuffer();
for(String str:strList){
sb.append(str);
}
return sb.toString();
}
}
6、剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
2 <= n <= 58
解题思路:总体长度为n,每段分为x,共有n/x个x,总体乘积则为 f ( x ) = x n x f(x)=x^{\frac{n}{x}} f(x)=xxn 求最大值,取对数,提取 h ( x ) = l n x x h(x)=\frac{lnx}{x} h(x)=xlnx,可知x=e时f(x)取到最大值,而题目中显然是整数,再根据h(x)大小确定x=3时f(x)取到最大值,故应该尽可能将绳子分为长度为3的小段。n=3*a+b,当b=0,f(x)=3^a;当b=1,可以拆出一个3组成4,2 *2>1 *3, f(x)=3^(a-1) *4;当b=2,f(x)=3^a *2。当然,也可以通过观察总结规律得出结论。
class Solution {
public int cuttingRope(int n) {
if(n<4){
return n-1;
}
//n=3*a+b
int a=n/3,b=n%3;
if(b==0){
return (int)Math.pow(3,a);
}
if(b==1){
return (int)Math.pow(3,a-1)*4;
}
return (int)Math.pow(3,a)*2;
}
}
7、二叉树中和为某一值的路径
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
提示:
节点总数 <= 10000
解题思路:回溯算法。使用一个path记录每一次走过的路径,dfs深度遍历二叉树。首先在非空时压入root.val,并更新下一步要寻找的sum。接下来,非叶子节点,则递归遍历左右子树,恢复sum,path弹出最后一个元素;叶子节点,若path记录总和为sum,则压入最终结果中,path弹出最后一个元素,并返回,因为此时已经找到了一条路径。T(N)=O(N),S(N)=O(N).
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//代码可以进一步简化,但是这样比较容易看懂
class Solution {
List<List<Integer>> ans=new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<Integer> path=new ArrayList<>();
dfs(root,sum,path);
return ans;
}
public void dfs(TreeNode root,int sum,List<Integer> path){
if(root==null){
return;
}
//非叶子节点,遍历完左右子树后回溯sum与path,
//因为还要dfs root的兄弟节点,故sum也需回溯
path.add(root.val);
sum-=root.val;
dfs(root.left,sum,path);
dfs(root.right,sum,path);
sum+=root.val;
path.remove(path.size()-1);
//叶子节点,判断sum是否合格,回溯path并返回
if(root.left==null && root.right==null){
path.add(root.val);
sum-=root.val;
if(sum==0){
ans.add(new ArrayList<>(path));
}
path.remove(path.size()-1);
return;
}
}
}
8、字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:
1 <= s 的长度 <= 8
解题思路:
class Solution {
List<String> ans=new ArrayList<>();
public String[] permutation(String s) {
char[] arrayS=s.toCharArray();
Arrays.sort(arrayS);
helper(new boolean[arrayS.length],new LinkedList<Character>(),arrayS);
String[] res=new String[ans.size()];
for(int i=0;i<ans.size();++i){
res[i]=ans.get(i);
}
return res;
}
//visited记录是否包含,list记录已包含字符,s为char[]
public void helper(boolean[] visited,LinkedList<Character> list,char[] s){
//得到一种排列
if(list.size()==s.length){
StringBuffer sb=new StringBuffer();
for(char c:list){
sb.append(c);
}
ans.add(sb.toString());
return;
}
for(int i=0;i<s.length;++i){
//从第二位开始,判断是否与前一位相同且前一位被包含
//为什么判断是否与前一位相同?比如先选中a,之后递归到i=1,会跳过b
if(i!=0 && s[i]==s[i-1] && visited[i-1]){
//继续尝试下一位字符
continue;
}
if(!visited[i]){
list.addLast(s[i]);
visited[i]=true;
helper(visited,list,s);
//回溯,visited与list复原
visited[i]=false;
list.removeLast();
}
}
}
}
9、把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
提示:
0 <= num < 231
解题思路:类似于跳台阶问题,可以使用动态规划来解决。
如果i和i-1位不能合并,即>25或者是i-1位为’0’,i的翻译方法就等于i-1的翻译加上i代表的字母,与i-1翻译方法相同;
如果i和i-1为可以合并,那么翻译方法就等于i-1的翻译+i代表字母,i-2翻译+(i-1,i)代表字母,种树等于i-2翻译方法+i-1翻译方法。
dp[i]=dp[i-1],i-1与i不能有效组合
dp[i]=dp[i-1]+dp[i-2],i-1和i可以有效组合
class Solution {
public int translateNum(int num) {
char[] str=String.valueOf(num).toCharArray();
int[] dp=new int[str.length+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=str.length;++i){
//当前与上一位无法有效组合
//即>25或者上一位为‘0’
if((str[i-2]-'0')*10+str[i-1]-'0'>25 || str[i-2]=='0'){
dp[i]=dp[i-1];
}
else{
dp[i]=dp[i-2]+dp[i-1];
}
}
return dp[str.length];
}
}
10、队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:
输入:
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
限制:
1 <= push_back,pop_front,max_value的总操作数 <= 10000
1 <= value <= 10^5
解题思路:类似于最小栈问题,肯定要使用辅助队列来解决,但是如果使用Queue先进先出,每次存储最大值,显然肯定是小值先poll,考虑使用Deque。
使用Deque来保存最大值,Queue插入元素后,先将Deque尾部所有小于元素的值弹出,确保Deque递减,然后每次pop_front之前,判断Deque头部是否等于Queue头部,小值的弹出并不会影响较大值的存储。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6OLNhVi-1598193666884)(https://pic.leetcode-cn.com/a151d4156944cef8165b17daca7a3131bcfc55c8a1be09cefa4cd6cdd8c03909-%E6%BC%94%E7%A4%BA%E6%96%87%E7%A8%BF1.gif)]
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> maxQueue;
public MaxQueue() {
super();
queue=new LinkedList<>();
maxQueue=new LinkedList<>();
}
public int max_value() {
return maxQueue.size()>0?maxQueue.peek():-1;
}
public void push_back(int value) {
queue.offer(value);
//maxQueue队尾所有小于value元素删除
//确保value前边没有比他小的元素
while(maxQueue.size()>0 && maxQueue.peekLast()<value){
maxQueue.pollLast();
}
maxQueue.offerLast(value);
}
public int pop_front() {
int temp=queue.size()>0?queue.poll():-1;
//maxQueue队首恰好为最大元素,出队
if(maxQueue.size()>0 && maxQueue.peek()==temp){
maxQueue.poll();
}
return temp;
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
11、二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
参考以下这颗二叉搜索树:
5
/ \
2 6
/ \
1 3
示例 1:
输入: [1,6,3,2,5]
输出: false
示例 2:
输入: [1,3,2,6,5]
输出: true
提示:
数组长度 <= 1000
解题思路:后序遍历的序列汇总,最后一个为根节点,前边半部分为左子树,节点值都比根节点小,后边半部分为右子树,节点值都比根节点大。可以利用这个性质,编写helper(int[] order,int left,int right)。
left>=right,能走到这一步说明之前都没问题,true;然后先找到第一个比rootValue(order[right])大的值,然后如果后面有比rootValue小的值,说明顺序错误,false;然后循环判断左右子树,确定是否返回false;如果前边的考验都通过了,那就没有问题啦,true。
class Solution {
public boolean verifyPostorder(int[] postorder) {
if(postorder.length<2){
return true;
}
return helper(postorder,0,postorder.length-1);
}
public boolean helper(int[] postorder,int left,int right){
//前边考验都通过
if(left>=right){
return true;
}
//找到第一个比rootValue大的值,进入右子树
int k=left;
int rootValue=postorder[right];
while(k<right && postorder[k]<rootValue){
++k;
}
//右子树中发现比rootValue小的值
for(int i=k;i<right;++i){
if(postorder[i]<rootValue){
return false;
}
}
//判断左右子树
if(!helper(postorder,left,k-1)){
return false;
}
if(!helper(postorder,k,right-1)){
return false;
}
return true;
}
}
12、机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
解题思路:DFS。
深度优先搜索: 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝 。
算法解析:
递归参数: 当前元素在矩阵中的行列索引 i 和 j ,两者的数位和 sumI, sumJ。
终止条件: 当 ① 行列索引越界 或 ② 数位和超出目标值 k 或 ③ 当前元素已访问过 时,返回 0 ,代表不计入可达解。
递推工作:
标记当前单元格 :将索引 (i, j) 存入 visited 中,代表此单元格已被访问过。
搜索下一单元格: 计算当前元素的 下、右 两个方向元素的数位和,并开启下层递归 ,上、左方向计算会返回0,所以直接省略了。
回溯返回值: 返回 1 + 右方搜索的可达解总数 + 下方搜索的可达解总数,代表从本单元格递归搜索的可达解总数。
class Solution {
private int m;
private int n;
private int k;
boolean[][] visited;
public int movingCount(int m, int n, int k) {
this.m=m;
this.n=n;
this.k=k;
visited=new boolean[m][n];
return dfs(0,0,0,0);
}
public int dfs(int x,int y,int sumX,int sumY){
//超出格子/位数和错误/已经访问过的情况
if(x<0 || x>=m || y<0 || y>=n ||sumX+sumY>k || visited[x][y]){
return 0;
}
//记录已走过,向右向下继续走
visited[x][y]=true;
return 1+dfs(x+1,y,(x+1)%10==0?sumX-8:sumX+1,sumY)
+dfs(x,y+1,sumX,(y+1)%10==0?sumY-8:sumY+1);
}
}
13、重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
限制:
0 <= 节点个数 <= 5000
解题思路:重点就是找出根节点的两个位置,划分出左右子树,需要一个helper函数,除了preorder和inorder,还需要将两组begin/end传进去。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return helper(preorder,0,preorder.length-1,
inorder,0,inorder.length-1);
}
public TreeNode helper(int[] preorder,int preBegin,int preEnd,
int[] inorder,int inBegin,int inEnd){
//遍历结束
if(preBegin>preEnd){
return null;
}
int rootValue=preorder[preBegin];
TreeNode root=new TreeNode(rootValue);
if(preBegin==preEnd){
return root;
}
//找到根节点在中序遍历中的位置
int rootInorder=inBegin;
for(int i=inBegin;i<=inEnd;++i){
if(rootValue==inorder[i]){
rootInorder=i;
}
}
int leftLength=rootInorder-inBegin;
int rightLength=inEnd-rootInorder;
TreeNode left=helper(preorder,preBegin+1,preBegin+leftLength,
inorder,inBegin,rootInorder-1);
TreeNode right=helper(preorder,preBegin+leftLength+1,preEnd,
inorder,rootInorder+1,inEnd);
root.left=left;
root.right=right;
return root;
}
}
14、剪绳子 II
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m] 。请问 k[0]*k[1]*...*k[m] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
2 <= n <= 1000
解题思路:关键就是大数取余问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6RgeFcv-1598193666886)(…/AppData/Roaming/Typora/typora-user-images/image-20200321163956167.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHONiD7c-1598193666887)(…/AppData/Roaming/Typora/typora-user-images/image-20200321164013606.png)]
class Solution {
public int cuttingRope(int n) {
if(n<=3){
return n-1;
}
//n=3*a+b
long res=1,x=3,p=1000000007;
int a=n/3,b=n%3;
//求3^(a-1)
for(a=n/3-1;a>0;a/=2){
if(a%2==1){
res=(res*x)%p;
}
x=(x*x)%p;
}
if(b==2){
res=(int)((res*6)%p);
}else if(b==1){
res=(int)((res*4)%p);
}else{
res=(int)((res*3)%p);
}
return (int)res;
}
}
15、矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
提示:
1 <= board.length <= 200
1 <= board[i].length <= 200
解题思路:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0dF0iEJh-1598193666890)(…/AppData/Roaming/Typora/typora-user-images/image-20200321170509602.png)]
class Solution{
public boolean exist(char[][] board, String word) {
char[] wordArr=word.toCharArray();
for(int row=0;row<board.length;row++){
for(int col=0;col<board[0].length;col++){
if(helper(board,wordArr,row,col,0)){
return true;
}
}
}
return false;
}
public boolean helper(char[][] board,char[] wordArr,int row,int col,int index){
//异常退出条件,一定先判断越界,再判断字符
if(row>=board.length || row<0 || col>=board[0].length || col<0 || board[row][col]!=wordArr[index]){
return false;
}
//正常退出条件
if(index==wordArr.length-1){
return true;
}
//保存当前字符,并重置,防止重新访问
char tmp=board[row][col];
board[row][col]='/';
boolean res=helper(board,wordArr,row,col+1,index+1)index
|| helper(board,wordArr,row,col-1,index+1)
|| helper(board,wordArr,row+1,col,index+1)
|| helper(board,wordArr,row-1,col,index+1);
//回溯
board[row][col]=tmp;
return res;
}
}
16、树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
示例 1:
输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
限制:
0 <= 节点个数 <= 10000
解题思路:先找到根节点,然后判断是否一致,但要注意并非完全一致。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B==null){
return false;
}
//层级遍历
Queue<TreeNode> queue=new LinkedList<>();
queue.offer(A);
//找到A与B根节点一致的节点
while(!queue.isEmpty()){
int size=queue.size();
for(int i=0;i<size;++i){
TreeNode node=queue.poll();
//判断两棵树是否一致
if(node.val==B.val){
return helper(node,B);
}
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
return false;
}
public boolean helper(TreeNode A,TreeNode B){
if(B==null){
return true;
}
if(A==null || A.val!=B.val){
return false;
}
return (helper(A.left,B.left)) &&
(helper(A.right,B.right));
}
}
17、表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、"5e2"、"-123"、"3.1416"、"0123"及"-1E-16"都表示数值,但"12e"、"1a3.14"、"1.2.3"、"+-5"及"12e+5.4"都不是。
解题思路:+/–数字-小数点-e/E-+/–数字
class Solution {
public boolean isNumber(String s) {
if(s==null || s.length()==0){
return false;
}
boolean numSeen=false;
boolean dotSeen=false;
boolean eSeen=false;
char[] str=s.trim().toCharArray();
for(int i=0;i<str.length;++i){
if(str[i]>='0' && str[i]<='9'){
numSeen=true;
}
else if(str[i]=='.'){
//.之前不能出现.或者e
if(dotSeen || eSeen){
return false;
}
dotSeen=true;
}
else if(str[i]=='e' || str[i]=='E'){
//e之前不能出现e,必须出现数字
if(eSeen || !numSeen){
return false;
}
eSeen=true;
//重置,e后可以再次出现数字
numSeen=false;
}
else if(str[i]=='-' || str[i]=='+'){
//-/+只能出现在第一位,或者e后边
if(i!=0 && str[i-1]!='e' && str[i-1]!='E'){
return false;
}
}
else{
//其他不合法字符
return false;
}
}
//e之后必须出现数字
return numSeen;
}
}
18、数值的整数次方
实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。
class Solution {
public double myPow(double x, int n) {
double ans=1,temp=x;
int exr=n;
while(exr!=0){
if((exr%2)!=0){
ans*=temp;
}
temp*=temp;
exr=exr/2;
}
return n>0?ans:1/ans;
}
}
19、从上到下打印二叉树,层级遍历,送分题
List<Integer>->int[]
list.stream().mapToInt(Integer::valueOf).toArray()
20、复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
提示:
-10000 <= Node.val <= 10000
Node.random 为空(null)或指向链表中的节点。
节点数目不超过 1000 。
解题思路:深拷贝知识点,第一遍复制val,第二遍复制指针。难点就是第二遍复制指针的时候,要可以定位到原表节点对应的新表节点,而不是单纯的val相等,使用HashMap<Node,Node>可以很好的解决。
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
Map<Node,Node> map=new HashMap<>();
//第一遍复制val
Node node=head;
while(node!=null){
map.put(node,new Node(node.val));
node=node.next;
}
//第二遍复制指针,一定要找到对应关系
node=head;
while(node!=null){
map.get(node).next=map.get(node.next);
map.get(node).random=map.get(node.random);
node=node.next;
}
}
}
21、1~n整数中1出现的次数
输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
示例 1:
输入:n = 12
输出:5
示例 2:
输入:n = 13
输出:6
限制:
1 <= n < 2^31
解题思路:f(n))函数的意思是1~n这n个整数的十进制表示中1出现的次数,将n拆分为两部分,最高一位的数字high和其他位的数字last,分别判断情况后将结果相加,看例子更加简单。
例子如n=1234,high=1, pow=1000, last=234
可以将数字范围分成两部分1999和10001234
1~999这个范围1的个数是f(pow-1)
1000~1234这个范围1的个数需要分为两部分:
千分位是1的个数:千分位为1的个数刚好就是234+1(last+1),注意,这儿只看千分位,不看其他位
其他位是1的个数:即是234中出现1的个数,为f(last)
所以全部加起来是f(pow-1) + last + 1 + f(last);
例子如3234,high=3, pow=1000, last=234
可以将数字范围分成两部分1999,10001999,20002999和30003234
1~999这个范围1的个数是f(pow-1)
1000~1999这个范围1的个数需要分为两部分:
千分位是1的个数:千分位为1的个数刚好就是pow,注意,这儿只看千分位,不看其他位
其他位是1的个数:即是999中出现1的个数,为f(pow-1)
2000~2999这个范围1的个数是f(pow-1)
3000~3234这个范围1的个数是f(last)
所以全部加起来是pow + high*f(pow-1) + f(last);
class Solution {
public int countDigitOne(int n) {
if(n<=0){
return 0;
}
//n=high*pow+last
//如3234=3*1000+234
char[] arr=String.valueOf(n).toCharArray();
int pow=(int)Math.pow(10.0,arr.length-1);
int high=arr[0]-'0';
int last=n-high*pow;
if(high==1){
return countDigitOne(pow-1)+countDigitOne(last)+last+1;
}
return pow+high*countDigitOne(pow-1)+countDigitOne(last);
}
}
22、数组中数字出现的次数
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:
输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
限制:
2 <= nums <= 10000
解题思路:出现一次的数字如果只有一个很好求,全部做一遍异或就可以了。因为有两个数字,做完异或一定是有某一位二进制不一样,也就是^后从右往左第一位为1的位,然后所有num对这个做&,就可以根据2个不同的数字分为两组,分别做异或即可。
class Solution {
public int[] singleNumbers(int[] nums) {
//s是只出现一次的2个数字的^ 记做数字a,b
//既然a,b 不一样,那么s肯定不是0,那么s的二进制肯定至少有1位(第n位)是1,只有0^1才等于1
//所以a,b 在第n位,要么a是0,b是1 ,要么b是0,a是1 ---->A
//s = 3 ^ 5; 0x0011 ^ 0x0101 = 0x0110 = 6
//假设int是8位
//-6 原码1000 0110
// 反码1111 0001
// 补码1111 0010
//s & (-s)
// 0000 0110
//& 1111 0010
// 0000 0010
//所以s = s & (-s) 就是保留s的最后一个1,并且将其他位变为0 也就是s最后一个1是倒数第二位 --->B
//由于s & (-s)很方便找到一个1 所以用他了,其实找到任何一个1都可以
//根据A和B 我们可以确定 3 和 5 必定可以分到 不同的组里
//同理 1和1 由于二进制完全相同,所有必定分到相同的组里
int s=0;
int[] res=new int[2];
for(int num:nums){
s^=num;
}
s&=(-s);
//1 0001 第一组
//2 0010 第二组
//1 0001 第一组
//3 0011 第二组
//2 0010 第二组
//5 0101 第一组
//第一组 1 1 5 第二组 2 3 2 这样我们就将2个只有一个的数 分到了2个数组里了
for(int num:nums){
if((num&s)==0){
res[0]^=num;
}else{
res[1]^=num;
}
}
return res;
}
}
23、 数组中数字出现的次数 II
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
示例 1:
输入:nums = [3,4,3,3]
输出:4
示例 2:
输入:nums = [9,1,7,9,7,9,7]
输出:1
限制:
1 <= nums.length <= 10000
1 <= nums[i] < 2^31
class Solution {
//0011/0011/0011/0100
//各位1出现次数分别为0133
//0/3可被整除,说明目标数字为0100=4
public int singleNumber(int[] nums) {
int res=0;
//遍历32位
for(int i=0;i<32;++i){
int count=0;
int bit=1<<i;
for(int num:nums){
//计算当前位为1的数量
if((num&bit)==1){
count++;
}
}
//不能被3整除,说明目标数字此位为1
if(count%3!=0){
res+=bit;
}
}
return res;
}
}
24、礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
class Solution {
public int maxValue(int[][] grid) {
int totalRow=grid.length,totalCol=grid[0].length;
//初始化dp边界值,0行0列
for(int col=1;col<totalCol;++col){
grid[0][col]+=grid[0][col-1];
}
for(int row=1;row<totalRow;++row){
grid[row][0]+=grid[row-1][0];
}
//从左边或者上边过来
for(int row=1;row<totalRow;++row){
for(int col=1;col<totalCol;++col){
grid[row][col]+=Math.max(grid[row-1][col],grid[row][col-1]);
}
}
return grid[totalRow-1][totalCol-1];
}
}
class Solution {
public int maxValue(int[][] grid) {
int totalRow=grid.length,totalCol=grid[0].length;
int[] dp=new int[totalCol+1];
for(int row=0;row<totalRow;++row){
for(int col=0;col<totalCol;++col){
//此行和上一行总和最大值dp[col+1]/dp[col]
dp[col+1]=Math.max(dp[col],dp[col+1])+grid[row][col];
}
}
return dp[totalCol];
}
}
25、股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
限制:
0 <= 数组长度 <= 10^5
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0){
return 0;
}
int min=prices[0],curProfit=0,maxProfit=0;
for(int i=1;i<prices.length;++i){
min=Math.min(min,prices[i]);
//curProfit确保一定是后边的减去前边的
curProfit=Math.max(curProfit,prices[i]-min);
maxProfit=Math.max(curProfit,maxProfit);
}
return maxProfit;
}
}