- 数组中的逆序对
题目要求:
如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,
求出这个数组中的逆序对总数。例如输入{7,5,6,4},一共有5个逆序对,分别是
(7,6),(7,5),(7,4),(6,4),(5,4)。
解题思路:
思路1:暴力解决。顺序扫描数组,对于每个元素,与它后面的数字进行比较,因此这种
思路的时间复杂度为o(n^2)。
思路2:
上述思路在进行比较后,并没有将相关信息留下,其实在比较之后可以进行局部的排序,
从而降低比较的次数,降低时间复杂度。
可通过如下步骤求逆序对个数:先把数组逐步分隔成长度为1的子数组,统计出子数组内部
的逆序对个数,然后再将相邻两个子数组合并成一个有序数组并统计数组之间的逆序对数目,
直至合并成一个大的数组。其实,这是二路归并的步骤,只不过在归并的同事要多进行一步
统计。因此时间复杂度o(nlogn),空间复杂度o(n),如果使用原地归并排序,可以将空间
复杂度降为o(1)。
本文使用经典二路归并排序实现。以{7,5,6,4}为例,过程如下:
[7 5 6 4]
/ \ 分:将长度为4的数组分成长度为2的数组
[7 5] [6 4]
/ \ / \ 分:将长度为2的数组分成长度为1的数组
[7] [5] [6] [4]
\ / \ / 和:1->2,并记录子数组内的逆序对
[5 7] [4 6]
\ / 和:2->4,并记录子数组内的逆序对
[4 5 6 7]
public class P249_InversePairs {
public static int inversePairs(int[] data){
if(data==null || data.length<2)
return 0;
return mergeSortCore(data, 0, data.length - 1);
}
public static int mergeSortCore(int[] data,int start,int end){
if(start>=end)
return 0;
int mid = start+(end-start)/2;
int left = mergeSortCore(data,start,mid);
int right = mergeSortCore(data,mid+1,end);
int count = mergerSortMerge(data,start,mid,end);
return left+right+count;
}
//start~mid, mid+1~end
public static int mergerSortMerge(int[] data,int start,int mid,int end){
int[] temp = new int[end-start+1];
for(int i=0;i<=end-start;i++)
temp[i] = data[i+start];
int left = 0,right = mid+1-start,index = start,count = 0;
while (left<=mid-start && right<=end-start){
if(temp[left]<=temp[right])
data[index++] = temp[left++];
else{
data[index++] = temp[right++];
count += (mid-start)-left+1;
}
}
while (left<=mid-start)
data[index++] = temp[left++];
while (right<=end-start)
data[index++] = temp[right++];
return count;
}
public static void main(String[] args){
System.out.println(inversePairs(new int[]{7,5,6,4}));
System.out.println(inversePairs(new int[]{5,6,7,8,1,2,3,4}));
}
}
- 两个链表的第一个公共节点
题目要求:
输入两个单链表,找出它们的第一个公共节点。以下图为例,对一个公共节点为6所在的节点。
1 -> 2 -> 3 -> 6 -> 7
4 -> 5 ↗
解题思路:
1 暴力求解 o(mn) o(1)
2 分别存入两个栈,求栈中最深的相同节点 o(m+n) o(m+n)
3 长链表先行|n-m|步,转化为等长链表 o(m+n) o(1)
解法1:比较直接,不再赘述。
解法2:从链表的尾部向前看,发现尾部是相同的,向前走会分叉,找到分叉点即可。
按照这个思路,如果这是双向链表,即得到尾节点并能够得到每个节点的前一个节点,
那这个题就很简单了。对于单链表,本身是前节点->后节点,想要得到后节点->前节点,
可以借助于栈的后进先出特性。两个单链表分别入栈,得到[1,2,3,6,7],[4,5,6,7],
然后不断出栈即可找到分叉点。
解法3:对于单链表,如果从头结点开始找公共节点,一个麻烦点是两个链表长度可能
不一致,因此两个指针不同步(指两个指针无法同时指向公共点)。解决这个麻烦点,
整个问题也就能顺利解决。那么,能否让两个链表长度一致?长链表先行几步即可,
因为长链表头部多出的那几个节点一定不会是两链表的公共节点。以题目中的图为例,
可以让1所在的链表先向前移动1个节点到2,这样就转化为求下面这两个链表的第一个
公共节点:
2 -> 3 -> 6 -> 7
4 -> 5 ↗
一个指针指向2,另一个指向4,然后同时遍历,这应该很容易了吧。需要对两个链表先
进行一次遍历求出长度,然后再同时遍历求公共点,时间复杂度o(n),空间复杂度o(1)。
三种解法的代码实现如下。
package chapter5;
import structure.ListNode;
import java.util.Stack;
public class P253_CommonNodesInLists {
//思路一:暴力解决,时间复杂度o(mn),空间复杂度o(1)
//思路二:借助于两个栈,时间复杂度o(m+n),空间复杂度o(m+n)
//思路三:转化为等长链表,时间复杂度o(m+n),空间复杂度o(1)
public static ListNode findFirstCommonNode(ListNode head1,ListNode head2){
for(ListNode node1=head1;node1!=null;node1=node1.next){
for(ListNode node2=head2;node2!=null;node2=node2.next){
if(node1==node2)
return node1;
}
}
return null;
}
public static ListNode findFirstCommonNode2(ListNode head1,ListNode head2){
Stack> stack1 = new Stack<>();
Stack> stack2 = new Stack<>();
for(ListNode node1=head1;node1!=null;node1=node1.next)
stack1.push(node1);
for(ListNode node2=head2;node2!=null;node2=node2.next)
stack2.push(node2);
ListNode commonNode = null;
while (!stack1.isEmpty() && !stack2.isEmpty()){
if(stack1.peek()==stack2.peek()){
commonNode = stack1.pop();
stack2.pop();
}
else
break;
}
return commonNode;
}
public static ListNode findFirstCommonNode3(ListNode head1,ListNode head2){
ListNode node1 = head1,node2 = head2;
int size1 = 0,size2 = 0;
for (;node1!=null;node1 = node1.next)
size1++;
for (;node2!=null;node2 = node2.next)
size2++;
node1 = head1;
node2 = head2;
while (size1>size2){
node1 = node1.next;
size1--;
}
while (size2>size1){
node2 = node2.next;
size2--;
}
while (node1!=null){
if(node1!=node2){
node1 = node1.next;
node2 = node2.next;
}
else
break;
}
return node1;
}
public static void main(String[] args){
// 1->2->3->6->7
// 4->5↗
ListNode node1 = new ListNode<>(1);
ListNode node2 = new ListNode<>(2);
ListNode node3 = new ListNode<>(3);
ListNode node4 = new ListNode<>(4);
ListNode node5 = new ListNode<>(5);
ListNode node6 = new ListNode<>(6);
ListNode node7 = new ListNode<>(7);
node1.next = node2;
node2.next = node3;
node3.next = node6;
node4.next = node5;
node5.next = node6;
node6.next = node7;
ListNode commonNode = findFirstCommonNode(node1,node4);
System.out.println(commonNode.val);
ListNode commonNode2 = findFirstCommonNode2(node1,node4);
System.out.println(commonNode2.val);
ListNode commonNode3 = findFirstCommonNode2(node1,node4);
System.out.println(commonNode3.val);
}
}
- 数字在排序数组中出现的次数
题目要求:
统计一个数字在排序数组中出现的次数。例如,输入{1,2,3,3,3,3,4,5}
和数字3,由于3在这个数组中出现了4次,因此输出4。
解题思路:
排序数组,定位某一个数值的位置,很容易想到二分查找。分成两部分:求第一个
出现该值的位置start,求最后一个出现该值得位置end,end-start+1即为所求。
二分法比较关键的一个逻辑如下:
mid = left + (right - left + 1) / 2;
if (data[mid] > k)
right = mid-1;
else if(data[mid] < k)
left = mid+1;
else
right = mid;
mid = left + (right - left + 1) / 2;
if (data[mid] > k)
right = mid-1;
else if(data[mid] < k)
left = mid+1;
else
left = mid;
此处要注意求mid时如下两种写法的差别:
(1) mid = left + (right - left) / 2;
(2) mid = left + (right - left + 1) / 2;
如果left,right之前的数字个数为奇数,那两者没差异,比如left=2,right=6,
中间有3,4,5,那么(1)(2)求出的mid都是4。如果中间数字的个数是偶数,比如left=2,
right=5,中间有3,4,(1)求出的mid都是3,(2)求出的mid都是4,即(1)求出的mid偏左,
(2)求出的偏右。极端特殊情况下,比如有个数组data[7,7,7,7,7],求k=7出现的次数。
使用(1)的迭代过程为
left=0,right=4,mid=2
left=0,right=2,mid=1
left=0,right=1,mid=0 (注意这里)
left=0,right=0,left==right 且 data[left]=k,返回0
使用(2)的迭代过程为
left=0,right=4,mid=2
left=2,right=4,mid=3
left=3,right=4,mid=4(注意这里)
left=4,right=4,left==right 且 data[left]=k,返回4
4-0+1 = 5,即为所求。
另一个在用二分查找时,要注意的点是求mid时的写法:
(a) mid = (left + right) / 2;
(b) mid = left + (right - left) / 2;
推荐用(b),最好不要出现(a),当left、right都是比较大的数时,完成同样的功能,
(a)可能造成上溢,但(b)不会。
package chapter6;
public class P263_NumberOfK {
//遍历的话时间复杂度为o(n)
//考虑到数组是排序好的,可使用二分法,时间复杂度o(logn)
public static int getNumberOfK(int[] data,int k){
int start = getStartOfK(data,k);
if(start==-1)
return 0;
int end = getEndOfK(data,start,k);
return end-start+1;
}
public static int getStartOfK(int[] data,int k){
if(data[0]>k || data[data.length-1] k)
right = mid-1;
else if(data[mid] < k)
left = mid+1;
else
right = mid;
}
return -1;
}
public static int getEndOfK(int[] data,int start, int k){
int left = start,right = data.length-1,mid;
while (left<=right){
if(left==right){
if(data[left]==k)
return left;
else
return -1;
}
//当长度为2,mid取右值
mid = left + (right - left + 1) / 2;
if (data[mid] > k)
right = mid-1;
else if(data[mid] < k)
left = mid+1;
else
left = mid;
}
return -1;
}
public static void main(String[] args){
int[] data1 = new int[]{1,2,3,3,3,3,5,6};
int[] data2 = new int[]{1,2,3,3,3,3,4,5};
int[] data3 = new int[]{3,3,3,3,3,3,3,3};
System.out.println(getNumberOfK(data1,4));
System.out.println(getNumberOfK(data2,3));
System.out.println(getNumberOfK(data3,3));
}
}
53.2:0~n中缺失的数字
题目要求:
一个长度为n的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围
0~n之内。在范围0~n内的n个数字有且只有一个数字不在该数组中,请找出。
解题思路:
用二分法找到数组中第一个数值不等于下标的数字。
package chapter6;
public class P266_GetMissingNumber {
public static int getMissingNumber(int[] data){
int left = 0,right = data.length-1,mid;
while (left<=right){
//mid=left+(right-left)/2 用位运算替换除法
//注意加减法优先级高于位运算
mid = left+((right-left)>>1);
if(data[mid]==mid)
left = mid+1;
else
right = mid-1;
}
return left;
}
public static void main(String[] args){
int[] data1 = new int[]{0,1,2,3,4,5}; //6
int[] data2 = new int[]{0,1,3,4,5}; //2
int[] data3 = new int[]{1,2}; //0
System.out.println(getMissingNumber(data1));
System.out.println(getMissingNumber(data2));
System.out.println(getMissingNumber(data3));
}
}
53.3:数组中数值和下标相等的元素
题目要求:
假设一个单调递增的数组里的每个元素都是整数且是唯一的。编写一个程序,
找出数组中任意一个数值等于其下标的元素。例如,输入{-3,-1,1,3,5},输出3。
解题思路:
很基本的二分查找。。。
package chapter6;
public class P267_IntegerIdenticalToIndex {
public static int getNumberSameAsIndex(int[] data){
if(data==null ||data.length==0)
return -1;
int left = 0,right = data.length-1;
if(data[left]>0||data[right]<0)
return -1;
int mid;
while (left<=right){
mid = left+((right-left)>>1);
if(data[mid]==mid)
return mid;
else if(data[mid]
- 二叉搜索树的第k大节点
题目要求:
找出二叉搜索树的第k大节点。例如,在下图的树里,第3大节点的值为4,
输入该树的根节点,3,则输出4。
5
/ \
3 7
/ \ / \
2 4 6 8
解题思路:
二叉搜索树的中序遍历是有序的。可以引入一个计数器,每访问一个节点,
计数器+1,当计数器等于k时,被访问节点就是该二叉搜索树的第k大节点。
package chapter6;
import structure.TreeNode;
import java.util.Stack;
public class P269_KthNodeInBST {
public static TreeNode kthNode(TreeNode root,int k){
//栈顶元素保证一直是cur的父节点
if(root==null || k<0)
return null;
Stack> stack = new Stack<>();
TreeNode cur = root;
int count = 0;
while (!stack.isEmpty()||cur!=null){
if(cur!=null){
stack.push(cur);
cur = cur.left;
}
else{
cur = stack.pop();
count++;
if(count==k)
return cur;
cur = cur.right;
}
}
return null;
}
public static void main(String[] args){
TreeNode root = new TreeNode<>(5);
root.left = new TreeNode<>(3);
root.left.left = new TreeNode<>(2);
root.left.right = new TreeNode<>(4);
root.right = new TreeNode<>(7);
root.right.left = new TreeNode<>(6);
root.right.right = new TreeNode<>(8);
System.out.println(kthNode(root,3).val);//4
System.out.println(kthNode(root,6).val);//7
System.out.println(kthNode(root,8));//null
}
}
- 二叉树的深度
题目要求:
求二叉树的深度。仅仅包含一个根节点的二叉树深度为1。
解题思路:
二叉树root的深度比其子树root.left与root.right的深度的最大值大1。
因此可以通过上述结论递归求解。
如果不使用递归,可以通过层序遍历(广度优先遍历)解决。
package chapter6;
import structure.TreeNode;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class P271_TreeDepth {
//递归
public static int treeDepth(TreeNode root){
if(root==null)
return 0;
int left = treeDepth(root.left);
int right = treeDepth(root.right);
return left>right?(left+1):(right+1);
}
//非递归,广度优先/层序遍历
public static int treeDepth2(TreeNode root){
if(root==null)
return 0;
Queue> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while (!queue.isEmpty()){
int size = queue.size();
TreeNode cur = null;
for(int i=0;i root = new TreeNode<>(1);
root.left = new TreeNode<>(2);
root.left.left = new TreeNode<>(4);
root.left.right = new TreeNode<>(5);
root.left.right.left = new TreeNode<>(7);
root.right = new TreeNode<>(3);
root.right.right = new TreeNode<>(6);
System.out.println(treeDepth(root));
System.out.println(treeDepth2(root));
}
}
55.2:平衡二叉树
题目要求:
输入二叉树的根节点,判断该树是否是平衡二叉树。如果某二叉树的任意节点的
左右子树深度之差不超过1,则该树是平衡二叉树。
解题思路:
思路1:依据平衡二叉树的定义解决。借助于上一题二叉树的深度,从根节点开始
逐点判断树的左右子树的深度差值是否满足要求。由于此解法是从根到叶的判断,
每一次获取节点有需要从当前节点遍历到叶节点,因此需要多次遍历。
思路2:如果用后序遍历,那么访问某一个节点时已经访问了它的左右子树。同时,
在访问节点时记录下它的深度,并由左右子树的深度推出父亲节点的深度,这样
我们就能通过遍历一遍完成整棵树的平衡性判断。
对于某一个子树,需要给出它的平衡性的判断,还要给出它的深度,此处我将平衡
性的判断作为返回值,深度通过长度为1的数组传递出去,见
boolean isBalanced2Core(TreeNode node,int[] depth)。
package chapter6;
import structure.TreeNode;
public class P273_isBalanced {
//借助于深度,判断是否是平衡二叉树,由于是从根到叶逐点判断,需要多次遍历树
public static boolean isBalanced(TreeNode node){
if(node==null)
return true;
int left = treeDepth(node.left);
int right = treeDepth(node.right);
int diff = left - right;
if(diff<-1||diff>1)
return false;
return isBalanced(node.left)&&isBalanced(node.right);
}
public static int treeDepth(TreeNode root){
if(root==null)
return 0;
int left = treeDepth(root.left);
int right = treeDepth(root.right);
return left>right?(left+1):(right+1);
}
//用后序遍历,并记录每个节点的深度,从而可以通过一次遍历完成整棵树的判断
public static boolean isBalanced2(TreeNode node){
if(node==null)
return true;
return isBalanced2Core(node,new int[]{0});
}
public static boolean isBalanced2Core(TreeNode node,int[] depth){
if(node==null){
depth[0] = 0;
return true;
}
int[] left = new int[]{0};
int[] right = new int[]{0};
if(isBalanced2Core(node.left,left)&&isBalanced2Core(node.right,right)){
int diff = left[0]-right[0];
if(diff<=1&&diff>=-1){
depth[0] = 1+(left[0]>right[0]?left[0]:right[0]);
return true;
}
else
return false;
}
return false;
}
public static void main(String[] args){
TreeNode root = new TreeNode<>(1);
root.left = new TreeNode<>(2);
root.left.left = new TreeNode<>(4);
root.left.right = new TreeNode<>(5);
root.left.right.left = new TreeNode<>(7);
root.right = new TreeNode<>(3);
root.right.right = new TreeNode<>(6);
System.out.println(isBalanced(root));
System.out.println(isBalanced2(root));
}
}
- 数组中只出现一次的两个数字
题目要求:
一个整数数组里除了两个数字出现一次,其他数字都出现两次。请找出这两个数字。
要求时间复杂度为o(n),空间复杂度为o(1)。
解题思路:
这道题可以看成“数组中只出现一次的一个数字”的延伸。如果所有数字都出现两次,
只有一个数字是出现1次,那么可以通过把所有所有进行异或运算解决。因为x^x = 0。
但如果有两个数字出现一次,能否转化成上述问题?依旧把所有数字异或,最终的
结果就是那两个出现一次的数字a,b异或的结果。因为a,b不想等,因此结果肯定不为0,
那么结果的二进制表示至少有一位为1,找到那个1的位置p,然后我们就可以根据第p位
是否为1将所有的数字分成两堆,这样我们就把所有数字分成两部分,且每部分都是只包
含一个只出现一次的数字、其他数字出现两次,从而将问题转化为最开始我们讨论的
“数组中只出现一次的一个数字”问题。
实例分析(以2,4,3,6,3,2,5,5为例):
相关数字的二进制表示为:
2 = 0010 3 = 0011 4 = 0100
5 = 0101 6 = 0110
步骤1 全体异或:2^4^3^6^3^2^5^5 = 4^6 = 0010
步骤2 确定位置:对于0010,从右数的第二位为1,因此可以根据倒数第2位是否为1进行分组
步骤3 进行分组:分成[2,3,6,3,2]和[4,5,5]两组
步骤4 分组异或:2^3^6^3^2 = 6,4^5^5 = 4,因此结果为4,6。
package chapter6;
public class P275_NumberAppearOnce {
public static int[] findNumsAppearOnce(int[] data){
int result = 0;
for(int i=0;i>1;
indexOf1*=2;
}
}
return -1;
}
public static void main(String[] args){
int[] data = new int[]{2,4,3,6,3,2,5,5};
int[] result = findNumsAppearOnce(data); // 4,6
System.out.println(result[0]);
System.out.println(result[1]);
}
}
- 和为s的数字
题目要求:
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使它们的和为s。
如果有多对和为s,输入任意一对即可。
解题思路:
使用两个指针分别指向数组的头、尾。如果和大于s,头部指针后移,如果和小于s,尾部指针前移。
package chapter6;
public class P280_TwoNumbersWithSum {
public static int[] findNumbersWithSum(int[] data,int sum){
int[] result = new int[]{0,0};
if(data==null || data.length<2)
return result;
int curSum = data[0]+data[data.length-1];
int left = 0,right = data.length-1;
while (curSum!=sum && leftsum)
right--;
else
left++;
curSum = data[left]+data[right];
}
if(curSum==sum){
result[0] = data[left];
result[1] = data[right];
}
return result;
}
public static void main(String[] args){
int[] data = new int[]{1,2,4,7,11,15};
int[] result = findNumbersWithSum(data,15);
System.out.println(result[0]);
System.out.println(result[1]);
}
}
57.2:和为s的连续正数序列
题目要求:
输入一个整数s,打印所有和为s的连续正数序列(至少两个)。例如,输入15,
由于1+2+3+4+5=4+5+6=7+8=15,所以打印出三个连续序列15,46,7~8。
解题思路:
与上一题类似,依旧使用两个指针small,big,值分别为1,2。如果从small
加到big的和等于s,即找到了一组解,然后让big后移,继续求解。如果和小于s,
big后移,如果和大于s,small前移。直到small大于s/2停止。
package chapter6;
public class P282_ContinuousSequenceWithSum {
public static void findContinuousSequence(int sum){
if(sum<3)
return;
int small = 1,big = 2,middle = sum>>1;
int curSum = small+big;
while (small<=middle){
if(curSum==sum){
printContinousSequence(small,big);
big++;
curSum+=big;
}
else if(curSum
- 翻转单词顺序
题目要求:
输入一个英文句子,翻转单词顺序,单词内字符不翻转,标点符号和普通字母
一样处理。例如输入输入“I am a student.”,则输出“student. a am I”。
解题思路:
首先字符串整体翻转,得到“.tneduts a ma I”;然后以空格作为分隔符进行切分,
对于切分下来的每一部分再进行翻转,得到“student. a am I”。
package chapter6;
public class P284_ReverseWordsInSentence {
public static String reverse(String str){
StringBuilder stringBuilder = new StringBuilder(str);
reverseSubString(stringBuilder,0,stringBuilder.length()-1);
int start = 0,end = stringBuilder.indexOf(" ");
while (start
58.2:左旋转字符串
题目要求:
实现一个函数完成字符串的左旋转功能。比如,输入abcdefg和数字2,输出为cdefgab。
解题思路:
类似于58.翻转单词顺序。首先对于字符串“abcdefg”整体翻转,得到“gfedcba”;
然后对于后2个字符“ba”进行翻转,对于剩下的字符“gfedc”进行翻转,得到“cdefgab”。
package chapter6;
public class P286_LeftRotateString {
public static String leftRotateString(String str,int i){
if(str==null||str.length()==0||i<=0||i>=str.length())
return str;
StringBuilder stringBuilder = new StringBuilder(str);
reverseSubString(stringBuilder,0,stringBuilder.length()-1);
reverseSubString(stringBuilder,0,stringBuilder.length()-i-1);
reverseSubString(stringBuilder,stringBuilder.length()-i,stringBuilder.length()-1);
return stringBuilder.toString();
}
//翻转stringBuilder[start,end]
public static void reverseSubString(StringBuilder stringBuilder,int start,int end){
for(int i=start;i<=start+(end-start)/2;i++){
char temp = stringBuilder.charAt(i);
stringBuilder.setCharAt(i,stringBuilder.charAt(end-i+start));
stringBuilder.setCharAt(end-i+start,temp);
}
}
public static void main(String[] args){
String str = "abcdefg";
System.out.println(leftRotateString(str,2));
}
}
- 滑动窗口的最大值
题目要求:
给定一个数组和滑动窗口的大小,请找出所有滑动窗口的最大值。例如,
输入数组{2,3,4,2,6,2,5,1}和数字3,那么一共存在6个滑动窗口,
他们的最大值分别为{4,4,6,6,6,5}。
解题思路:
思路1:使用暴力求解。假设滑动窗口长度为k,每到一个点都向前遍历k-1
个节点,那么总时间复杂度为o(nk)。
思路2:长度为k的滑动窗口其实可以看成一个队列,而滑动窗口的移动可以
看成队列的出队和入队,因此本题可以转化为求长度为k的队列的最大值。借助之前
做过的9.用两个栈实现队列和30.包含min函数的栈,我们可以实现求队列的最大值。
下面我们分析下时间与空间复杂度。用两个栈实现长度为k的队列的入队时间
复杂度o(1),出队时间复杂度o(k),空间复杂度为o(k);包含最值函数的栈
(最大深度为k)的时间复杂度为o(1),空间复杂度为o(k)。因此长度为k的
队列的一次出队+入队操作(即窗口前移一位)时间复杂度为o(k),空间复
杂度为o(k)。而这个窗口需要完成在长度为n的数组上从头到尾的滑动,因此,
本解法的时间复杂度为o(nk),空间复杂度o(k)。
思路3:把可能成为最大值数字的下标放入双端队列deque,从而减少遍历次数。
首先,所有在没有查看后面数字的情况下,任何一个节点都有可能成为某个状态
的滑动窗口的最大值,因此,数组中任何一个元素的下标都会入队。关键在于出队,
以下两种情况下,该下标对应的数字不会是窗口的最大值需要出队:(1)该下标已
经在窗口之外,比如窗口长度为3,下标5入队,那么最大值只可能在下标3,4,5中
出现,队列中如果有下标2则需要出队;(2)后一个元素大于前面的元素,那么前面
的元素出对,比如目前队列中有下标3、4,data[3] = 50,data[4]=40,下标5入
队,但data[5] = 70,则队列中的3,4都需要出队。
数组{2,3,4,2,6,2,5,1}的长度为3的滑动窗口最大值求解步骤如下
步骤 插入数字 滑动窗口 队列中的下标 最大值
1 2 2 0(2) N/A
2 3 2,3 1(3) N/A
3 4 2,3,4 2(4) 4
4 2 3,4,2 2(4),3(2) 4
5 6 4,2,6 4(6) 6
6 2 2,6,2 4(6),5(2) 6
7 5 6,2,5 4(6),6(5) 6
8 1 2,5,1 6(5),7(1) 5
时间复杂度在o(n)~o(nk)之间,空间复杂度o(k)。
思路3的代码实现如下:
package chapter6;
import java.util.ArrayDeque;
import java.util.Deque;
public class P288_MaxInSlidingWindow {
//把可能会成为最大值的下标存储下来,从而降低扫描次数
public static int[] maxInWindows(int[] data,final int size){
if(data==null ||data.length==0||data.length deque = new ArrayDeque<>();
for(int i=0;i=data[deque.getLast()])
deque.removeLast();
deque.addLast(i);
}
for(int i=size-1;isize)
deque.removeFirst();
while (!deque.isEmpty()&&data[deque.getLast()]<=data[i])
deque.removeLast();
deque.addLast(i);
result[i-(size-1)] = data[deque.getFirst()];
}
return result;
}
public static void main(String[] args){
int[] data = new int[]{2,3,4,2,6,2,5,1};
int[] result = maxInWindows(data,3);
for(int i=0;i
59.2:队列的最大值
题目要求:
定义一个队列并实现函数max得到队列里的最大值。要求max,pushBack,
popFront的时间复杂度都是o(1)。
解题思路:
两个思路均延续自59.滑动窗口的最大值
思路1:借助之前做过的9.用两个栈实现队列和30.包含min函数的栈,
我们可以实现求队列的最大值。入队时间复杂度o(1),出队时间复杂度
o(n),获取最大值时间复杂度o(1),空间复杂度o(n)。
思路2:将上一题的滑动窗口看成一个队列即可。入队时间复杂度o(1),
出队时间复杂度o(1),调整记录下标的双端队列的时间复杂度最差为o(n),
获取最值得时间复杂度为o(1)。
思路2的代码实现如下:
package chapter6;
import java.util.*;
public class P292_QueueWithMax {
public static class QueueWithMax {
private Deque> queueData;
private Deque> queueMax;
private int currentIndex;
public QueueWithMax() {
this.queueData = new ArrayDeque<>();
this.queueMax = new ArrayDeque<>();
this.currentIndex = 0;
}
public T max(){
if(queueMax.isEmpty())
return null;
return queueMax.getFirst().value;
}
public void pushBack(T value){
//如果当前value比queueMax.getLast大则removeLast
while (!queueMax.isEmpty()&&value.compareTo(queueMax.getLast().value)>=0)
queueMax.removeLast();
InternalData addData = new InternalData<>(value,currentIndex);
queueMax.addLast(addData);
queueData.addLast(addData);
currentIndex++;
}
public T popFront(){
if(queueData.isEmpty())
return null;
InternalData delData = queueData.removeFirst();
//如果queueData中删除的queueData和queueMax头元素的index相同
//才在queueMax中移除头元素
if(delData.index==queueMax.getFirst().index)
queueMax.removeFirst();
return delData.value;
}
private static class InternalData {
public M value;
public int index;
public InternalData(M value,int index){
this.value = value;
this.index = index;
}
}
}
public static void main(String[] args) {
QueueWithMax queue = new QueueWithMax<>();
queue.pushBack(3);
System.out.println(queue.max());
queue.pushBack(5);
System.out.println(queue.max());
queue.pushBack(1);
System.out.println(queue.max());
System.out.println("开始出队后,调用max");
System.out.println(queue.max());
queue.popFront();
System.out.println(queue.max());
queue.popFront();
System.out.println(queue.max());
queue.popFront();
System.out.println(queue.max());
}
}
- n个骰子的点数
题目要求:
把n个骰子仍在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s
的所有可能的值的出现概率。
解题思路:
新加入一个骰子,它出现1-6的概率是相等的,可以看成各出现一次,那么
出现和为s的次数等于再加入之前出现和为s-1,s-2,s-3,s-4,s-5,s-6这
6种情况的次数之和。如此循环,直到加入n个骰子结束。
package chapter6;
public class P294_DicesProbability {
public static void printProbability(int number){
if(number<=0)
return;
int result[][] = new int[2][6*number+1];
for(int i=1;i<=6;i++)
result[1][i] = 1;
for (int num=2;num<=number;num++){
for(int i=num;i<6*num+1;i++){
for(int j=i-6;j0)
result[num%2][i] += result[(num-1)%2][j];
}
}
double sum = 0;
for(int i=number;i<6*number+1;i++)
sum += result[number%2][i];
System.out.println("number = "+number);
for(int i=number;i<6*number+1;i++)
System.out.println("probability "+i+":"+result[number%2][i]/sum);
}
public static void main(String[] args){
printProbability(2);
printProbability(0);
printProbability(11);
}
}
- 扑克牌中的顺子
题目要求:
抽取5张牌,判断是不是一个顺子。2-10为数字本身,A为1,J为11,
Q为12,K为13,大小王可堪称任意数字。
解题思路:
将1-10,J,Q,K记作1-13,大小王记作0,0可以转化为任意的1-13的数字,
判断输入的5个数字是否能组成连续的5个数字即可。
package chapter6;
public class P298_ContinousCards {
public static boolean isContinous(int[] data){
if(data==null || data.length!=5)
return false;
int[] table = new int[14];
for(int i=0;i13||data[i]<0)
return false;
table[data[i]]++;
}
int start = 1;
while (table[start]==0)
start++;
int king = table[0];
for(int i=start;i13)
break;
if(table[i]>1||table[i]<0)
return false;
else if(table[i]==0){
if(king==0)
return false;
else
king--;
}
}
return true;
}
public static void main(String[] args){
int[] data1 = new int[]{4,2,7,12,1}; //false
int[] data2 = new int[]{0,5,6,12,0}; //false
int[] data3 = new int[]{6,5,8,7,4}; //true
int[] data4 = new int[]{0,5,6,9,8}; //true
int[] data5 = new int[]{0,13,0,12,0}; //true
System.out.println(isContinous(data1));
System.out.println(isContinous(data2));
System.out.println(isContinous(data3));
System.out.println(isContinous(data4));
System.out.println(isContinous(data5));
}
}
- 圆圈中最后剩下的数字
题目要求:
0,1,2...n-1这n个数字拍成一个圆圈,从数字0开始,每次从这个圆圈里
删除第m个数字,求剩下的最后一个数字。例如0,1,2,3,4这5个数字组成
的圈,每次删除第3个数字,一次删除2,0,4,1,因此最后剩下的是3。
解题思路:
最直接的思路是用环形链表模拟圆圈,通过模拟删除过程,可以得到最后剩
下的数字,那么这道题目就变成了删除链表中某一个节点。假设总节点数为n,
删除一个节点需要走m步,那么这种思路的时间复杂度为o(mn),空间复杂度o(n)。
思路2比较高级,较难理解,可遇不可求。将圆圈表示成一个函数表达式,将删除
节点的过程表示成函数映射的变化,时间复杂度o(n),空间复杂度o(1)!有兴趣
的话可以搜素”约瑟夫环“去详细了解。
package chapter6;
import structure.ListNode;
public class P300_LastNumberInCircle {
public static int lastRemaining(int n,int m){
if(n<1||m<1)
return -1;
ListNode head = new ListNode<>(0);
ListNode cur = head;
for(int i=1;i node = new ListNode<>(i);
cur.next = node;
cur = cur.next;
}
cur.next = head;
cur = head;
while (true){
//长度为1结束循环
if(cur.next==cur)
return cur.val;
//向后移动
for(int i=1;i
- 股票的最大利润
题目要求:
求买卖股票一次能获得的最大利润。例如,输入{9,11,8,5,7,12,16,14},
5的时候买入,16的时候卖出,则能获得最大利润11。
解题思路:
遍历过程中记录最小值min,然后计算当前值与min的差值diff,
并更新maxDiff,maxDiff=max(diff)。
package chapter6;
public class P304_MaximalProfit {
public static int maxDiff(int[] data){
if(data==null||data.length<2)
return 0;
int min = data[0];
int maxDiff = data[1] - min;
if(data[1]maxDiff)
maxDiff = data[i]-min;
if(data[i]
- 求1+2+...+n
题目要求:
求1+2+...+n,要求不能使用乘除法,for,while,if,else,switch,case等关键词及条件判断语句?:。
解题思路:
不能用循环,那么可以使用递归调用求和。但又不能使用if,结束条件如何生效?可以使用如下形式替代if语句
替代if的一种方式:boolean b=判断条件&&(t=递归执行语句)>0
b并没有实际的用途,只是为了使表达式完成。当判断条件不满足,递归也就结束了。
package chapter6;
public class P307_Accumulate {
public static int getSum(int num){
int t=0;
boolean b = (num>0)&&((t=num+getSum(num-1))>0);
return t;
}
public static void main(String[] args){
System.out.println(getSum(10));
}
}
- 不用加减乘除做加法
题目要求:
写一个函数,求两个正数之和,要求在函数体内不能使用四则运算符号。
解题思路:
不能用四则运算,那只能通过位运算了。其实四则运算是针对十进制,
位运算是针对二进制,都能用于运算。下面以0011(即3)与0101(即5)
相加为例说明
1.两数进行异或: 0011^0101=0110 这个数字其实是把原数中不需
进位的二进制位进行了组合
2.两数进行与: 0011&0101=0001 这个数字为1的位置表示需要进位,
而进位动作是需要向前一位进位
3.左移一位: 0001<<1=0010
此时我们就完成0011 + 0101 = 0110 + 0010的转换
如此转换下去,直到其中一个数字为0时,另一个数字就是原来的两个数字的和
package chapter6;
public class P310_AddTwoNumbers {
public static int add(int a,int b){
int sum = a^b;
int carry = (a&b)<<1;
int temp;
while (carry!=0){
temp = sum;
sum = sum^carry;
carry = (carry&temp)<<1;
}
return sum;
}
public static void main(String[] args){
System.out.println(add(3,5)); //8
System.out.println(add(3,-5)); //-2
System.out.println(add(0,1)); //1
}
}
不使用新的变量完成交换两个原有变量的值
题目要求:
不使用新的变量完成交换两个原有变量的值。
解题思路:
有加减法与亦或法两种,其实思路是一致的。推荐亦或法,不仅仅是代码上更简单,
而且亦或的运算在理论上也要比加减法更快。
package chapter6;
public class P312_ExchangeTwoNumbers {
public static void main(String[] args){
//基于加减法
int a = 3;
int b = 5;
a = a + b;
b = a - b;
a = a - b;
System.out.println("a="+a+",b="+b);
//基于异或法
a = 3;
b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a="+a+",b="+b);
}
}
- 构建乘积数组
题目要求:
给定数组A[0,1...n-1],求B[0,1...n-1],要求
B[i] = A[0]*A[1]...A[i-1]*A[i+1]...A[n-1],不能使用除法。
解题思路:
如果没有不能用除法的限制,可以通过∑A/A[i]求得B[i],
同时要注意A[i]等于0的情况。
既然不能使用除法,那就只好用纯乘法计算。如果每一个B中的元素都用
乘法计算,那么时间复杂度为0(n^2)。
使用纯乘法,还有更高效的思路。定义C[i]=A[0]*A[1]...A[i-1],
那么C[i]=C[i-1]*A[n-1];定义D[i]=A[i+1]*...A[n-2]*A[n-1],
那么D[i] =D[i+1]*A[i+1];因此C[i],D[i]都可以递推地求出来,
且B[i]=C[i]*D[i]。步骤如下:
1.计算C数组;
2.依次求得D[0],D[1]...D[n-1],同时完成B[i]=C[i]\*D[i]的计算。
如果最终结果存放于result[]中,第一步的C的值就可以先放到result中,
而D的元素可以存放于一个变量temp中,
通过result[i] = temp\*result[i]求得B。
时间复杂度o(n),由于计算B必然要申请长度为n的空间,除此之外,
该方法仅申请了几个变量,空间复杂度o(1)。
package chapter6;
public class P313_ConstructArray {
public static int[] multiply(int[] data){
if(data==null||data.length<2)
return null;
int[] result = new int[data.length];
//求得数组C,存于result中
result[0] = 1;
for(int i=1;i=0;i--){
//数组D中的元素值
temp = temp * data[i+1];
//计算B[i]=C[i]*D[i]
result[i] = result[i] * temp;
}
return result;
}
public static void main(String[] args){
int[] data = new int[]{1,2,3,4,5};
int[] result = multiply(data);
for(int i=0;i
- 把字符串转换成整数
题目要求:
如题。
解题思路:
此题比较麻烦的点是特殊情况较多,需要考虑完全。
1.如果字符串前后有空格,要去除;
2.如果是空串或null,要特殊处理;
3.如果头部有多个正负号,要特殊处理;
4.如果数值超出int的范围,要特殊处理;比int的最大值还要大,已经上溢,
这肯定不能通过数字的大小比较,所以需要在字符串的状态下判断是否上溢或下溢。
5.遇到非数字的字符,则转换停止;
刚刚发现一个新的情况未做处理,在数字之前有多个0,如果0之后的数字有溢出情况,
而前面的0又没有去处,这种溢出就不会被捕获,还缺这个情况的判断。这种思路真心
不好,一条主线是转换成整数,中间穿插出很多特殊情况的分支,感觉就像在打补丁。
package chapter7;
public class P318_StringToInt {
// atoi的需求是这样的:
// 如果前面有空格,需要剔除空格;
// 剔除空格后,第一个字符串如果是+号,认为是正数;如果是-号,认为是负数;
// 后面的字符如果不是数字,那么返回0,如果是数字,返回实际的数字。遇到不是数字的字符,转换结束。
// 此外,要考虑空串问题,数值溢出问题[2^(-31) ~ 2^31-1]。
public static int strToInt(String str) throws Exception{
if(str==null || str.length()==0)
throw new Exception("待转换字符串为null或空串");
String MAX_INT_PLUS_1 = Integer.toString(Integer.MIN_VALUE).substring(1);
StringBuilder stringBuilder = new StringBuilder(str.trim());
int flag = 0; //记录无符号的正(2)正(1),负(-1),初始值(0)
if(stringBuilder.charAt(0)=='-')
flag = -1;
else if(stringBuilder.charAt(0)=='+')
flag = 1;
else if(stringBuilder.charAt(0)>='0'&&stringBuilder.charAt(0)<='9')
flag = 2;
else
return 0;
int endIndex = 1;
while (endIndex='0'&&stringBuilder.charAt(endIndex)<='9')
endIndex++;
if(flag==2){
if(stringBuilder.substring(0,endIndex).toString().compareTo(MAX_INT_PLUS_1)>=0)
throw new Exception("数值上溢,待转换字符串为"+str);
return Integer.parseInt(stringBuilder.substring(0,endIndex));
}
else{
if(flag==1&&stringBuilder.substring(1,endIndex).compareTo(MAX_INT_PLUS_1)>=0)
throw new Exception("数值上溢,待转换字符串为"+str);
if(flag==-1&&stringBuilder.substring(1,endIndex).compareTo(MAX_INT_PLUS_1)>0)
throw new Exception("数值下溢,待转换字符串为"+str);
if(flag==-1&&stringBuilder.substring(1,endIndex).compareTo(MAX_INT_PLUS_1)==0)
//此处注意,此种情况不能用绝对值*(-1),该绝对值已经超出正数的最大值
return Integer.MIN_VALUE;
return flag*Integer.parseInt(stringBuilder.substring(1,endIndex));
}
}
public static void funcTest(){
try {
System.out.println(strToInt(" 100")); //100
System.out.println(strToInt("-100")); //-100
System.out.println(strToInt("0")); //0
System.out.println(strToInt("-0"));//0
System.out.println(strToInt("1.23")); //1
System.out.println(strToInt("-1.23")); //-1
System.out.println(strToInt(".123")); //0
}
catch (Exception e){
e.printStackTrace();
}
}
public static void edgeTest(){
try {
System.out.println(strToInt("2147483647")); //2147483647
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("-2147483647")); //-2147483647
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("2147483647")); //2147483647
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("2147483648")); //上溢
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("-2147483648")); //-2147483648
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("-2147483649")); //下溢
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt(null)); //待转换字符串为null或空串
}
catch (Exception e){
System.out.println(e.getMessage());
}
try {
System.out.println(strToInt("")); //待转换字符串为null或空串
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
public static void main(String[] args){
funcTest();
edgeTest();
}
}
- 树中两个节点的最低公共祖先
题目要求:
输入一棵树的根节点,输入两个被观察节点,求这两个节点的最低(最近)公共祖先。
解题思路:
此题比较开放,主要是对于“树”没有做明确说明,所以原书中就对树的可能情况做了假设,
然后就衍生出多种思路
1)如果是二叉,搜索树:
遍历找到比第一个节点大,比第二个节点小的节点即可
2)如果是父子间有双向指针的树:
由下往上看,转化为找两个链表的第一个公共节点问题
3)如果只是一个包含父到子的指针的普通树:
3.1)如果不能使用额外空间,从根节点开始判断他的子树是否包含那两个节点,
找到最小的的子树即可
时间复杂度o(n^2)(此为最差,平均不太好算。。。),空间复杂度为o(1)
3.2) 如果能用额外空间,可以遍历两次(深度优先)获取根节点到那两个节点的路径,
然后求两个路径的最后一个公共节点。 时间复杂度o(n),空间复杂度o(logn)
(1)(2)比较简单。下面仅对(3),以下图所示的树为例,进行思路实现与求解验证
A
/ \
B C
/ \
D E
/ \ / | \
F G H I J
import java.util.*;
public class P326CommonParentInTree {
public static class CommonTreeNode{
public char val;
public List children;
public CommonTreeNode(char val){
this.val = val;
children = new LinkedList<>();
}
public void addChildren(CommonTreeNode... children){
for(CommonTreeNode child:children)
this.children.add(child);
}
}
// 3.1所述的解法
public static CommonTreeNode getLastParent1(CommonTreeNode root,CommonTreeNode node1,CommonTreeNode node2){
if(root==null || node1==null || node2==null || !isInSubTree(root,node1,node2))
return null;
CommonTreeNode curNode = root;
while (true){
for(CommonTreeNode child:curNode.children){
if(isInSubTree(child,node1,node2)){
curNode = child;
break;
}
if(child==curNode.children.get(curNode.children.size()-1))
return curNode;
}
}
}
public static boolean isInSubTree(CommonTreeNode root,CommonTreeNode node1,CommonTreeNode node2){
Queue queue = new LinkedList<>();
CommonTreeNode temp = null;
int count = 0;
queue.add(root);
while (count!=2 && !queue.isEmpty()){
temp = queue.poll();
if(temp==node1||temp==node2)
count++;
if(!temp.children.isEmpty())
queue.addAll(temp.children);
}
if(count==2)
return true;
return false;
}
// 3.2所述的解法
public static CommonTreeNode getLastParent2(CommonTreeNode root,CommonTreeNode node1,CommonTreeNode node2){
List path1 = new ArrayList<>();
List path2 = new ArrayList<>();
getPath(root,node1,path1);
getPath(root,node2,path2);
CommonTreeNode lastParent = null;
for(int i=0;i curPath){
if(root==node)
return true;
curPath.add(root);
for(CommonTreeNode child:root.children){
if(getPath(child,node,curPath))
return true;
}
curPath.remove(curPath.size()-1);
return false;
}
public static void main(String[] args){
CommonTreeNode root = new CommonTreeNode('A');
CommonTreeNode b = new CommonTreeNode('B');
CommonTreeNode c = new CommonTreeNode('C');
CommonTreeNode d = new CommonTreeNode('D');
CommonTreeNode e = new CommonTreeNode('E');
CommonTreeNode f = new CommonTreeNode('F');
CommonTreeNode g = new CommonTreeNode('G');
CommonTreeNode h = new CommonTreeNode('H');
CommonTreeNode i = new CommonTreeNode('I');
CommonTreeNode j = new CommonTreeNode('J');
root.addChildren(b,c);
b.addChildren(d,e);
d.addChildren(f,g);
e.addChildren(h,i,j);
System.out.println(getLastParent1(root,f,h).val);
System.out.println(getLastParent2(root,f,h).val);
System.out.println(getLastParent1(root,h,i).val);
System.out.println(getLastParent2(root,h,i).val);
}
}