剑指Offer思路代码记录
本篇主要记录自己刷剑指Offer的解题思路和代码(Java),题目资源来自牛客网。(💡持续更新)
JZ1 二维数组中的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路
(1)最简单的方法就是直接双重暴力查找,但是这样就浪费了我们数组的性质,不可取。
(2)利用数组每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序的性质,我们以右上角为起点,对数组进行查找二分查找。
- 如果当前值大于右上角,行数加1,去找比右上角大的数。
- 如果当前值小于右上角,列数减1,去找比右上角小的树。
- 后续重复同样的操作,直到找到,返回true。
- 没有找到返回false。
实现代码
public class Solution {
public boolean Find(int target, int [][] array) {
//获取最大列长度
int y = array[0].length;
//获取最大行长度
int x = array.length;
//使用二分,从右上角开始查找
int column = y -1;
int row = 0;
while(row < x && column >= 0){
int value = array[row][column];
if(target > value){
row++;
}else if(value > target){
column--;
}else{
//找到返回true
return true;
}
}
//未找到false
return false;
}
}
JZ2 替换空格
题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解题思路&实现代码
模拟,将我们的空格替换成%20
。
(1)可以使用额外的空间。对字符串进行遍历,然后使用StringBuilder进行字符串的拼接,遇到空格添加%20
,没有则直接添加。
public class Solution {
public String replaceSpace(StringBuffer str) {
//如果字符串为空的话,直接返回null
if(str == null) return null;
StringBuilder sb = new StringBuilder();
for(int i = 0; i < str.length(); i++){
if(str.charAt(i) == ' '){
sb.append("%20");
}else{
sb.append(str.charAt(i));
}
}
return sb.toString();
}
}
(2)不使用额外的空间。那么我们只能在原StringBuffer上面进行修改了,这里Java提供了replace方法来进行修改,一行代码就搞定。但是不建议使用,因为这样就少了考察意义。我们手动去实现的话,可以分为这三步。
- 首先统计我们的的空格数量。
- 对我们的数组进行扩容,保证空格替换成
%20
有足够的空间。 - 可以从前往后遍历,然后遇到空格替换,但是后面字符要不断移动,效率低下。可以从后往前遍历,每次只移动一个字符,建议使用。
public class Solution {
public String replaceSpace(StringBuffer str) {
//如果字符串为空的话,直接返回null
if(str == null) return null;
int num = 0; //用来统计空格的数量
for(int i = 0; i < str.length(); i++){
if(str.charAt(i) == ' ') num++;
}
int newLength = str.length() + num * 2; //计算所需要的字符串空间
int left = str.length() - 1; //记录替换前的str下标
int right = newLength - 1; //记录替换后的str下标
str.setLength(newLength); //对数组进行扩容,防止越界
while(left >= 0 && left < right){
if(str.charAt(left) == ' '){
//遇到空格替换后的str下标直接逐位添加%20并向前移动
str.setCharAt(right--,'0');
str.setCharAt(right--,'2');
str.setCharAt(right--,'%');
}else{
//非空格字符直接向新下标移动位置
str.setCharAt(right--,str.charAt(left));
}
left--;
}
return str.toString();
}
}
JZ3 从尾到头打印链表
题目描述
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
解题思路&实现代码
(1)正常遍历,将数据存进list里,然后再new一个list存一下反转的数据。缺点是会消耗但是会消耗额外的空间。
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//第一次遍历存进list
ArrayList<Integer> list = new ArrayList<>();
while(listNode != null){
list.add(listNode.val);
listNode = listNode.next;
}
//第二次对list里面的数据进行反转
ArrayList<Integer> res = new ArrayList<>();
for(int i = list.size() - 1; i >= 0; i--){
res.add(list.get(i));
}
return res;
}
}
(2)使用递归的方式可以实现可以只消耗一个list的空间。我们对链表递归到最后一个结点添加,然后返回上一级的时候再进行添加,最后我们的list就是一个反转的链表数据。注意的是这里我们要将我们的list设置为全局的了,避免递归重复new对象。
public class Solution {
ArrayList<Integer> list = new ArrayList<>();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//递归
if(listNode != null){
//找到最后一个节点开始添加
this.printListFromTailToHead(listNode.next);
list.add(listNode.val);
}
return list;
}
}
(3)这里可以可以使用list的特性,在第一个节点添加后,自动后移。同样可以实现并且不消耗多余的空间。
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
while(listNode != null){
//添加之后自动向后移动
list.add(0,listNode.val);
listNode = listNode.next;
}
return list;
}
}
JZ4 重建二叉树
题目描述
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题思路
解题思路就是先从前序遍历找到根节点,然后根据中序遍历分割左右子树,递归重复操作。
以上面样例为例:
-
首先找到根节点1,然后可以在中序遍历里面找到位置,进行切割成左右子树{4,7,2}和{5,3,8,6}。
-
这里我们又有了前序遍历{2,4,7}和中序遍历{4,7,2},然后再进行分割。根节点为2,左子树的前序遍历为{4,7},中序遍历为{4,7},右子树为null。
-
继续操作,根节点为{4},右子树为{7},左子树为null。然后一直返回上一级到初始根节点,我们的左子树就构造完成了。
-
同样的,右子树也是这样构造的。
下面提供两种实现代码。
实现代码
(1)找到根节点位置,复制数组进行递归子树
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if (pre.length == 0 || in.length == 0) {
return null;
}
TreeNode root = new TreeNode(pre[0]);
// 在中序中找到前序的根
for (int i = 0; i < in.length; i++) {
if (in[i] == pre[0]) {
// 左子树,注意 copyOfRange 函数,左闭右开
root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1), Arrays.copyOfRange(in, 0, i));
// 右子树,注意 copyOfRange 函数,左闭右开
root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length), Arrays.copyOfRange(in, i + 1, in.length));
break;
}
}
return root;
}
}
(2)原地数组递归,子树找到上一级根节点停
public class Solution {
int pre = 0,in = 0;
public TreeNode reConstructBinaryTree(int [] preorder,int [] inorder) {
return recursive(preorder,inorder,Integer.MAX_VALUE);
}
public TreeNode recursive(int[] preorder,int[] inorder,int stop){
//如果二叉树为空直接返回null
if(pre >= preorder.length) return null;
//当找到左右子树的分割点,代表找完一边
if(inorder[in] == stop){
in++;
return null;
}
int curVal = preorder[pre++];
TreeNode cur = new TreeNode(curVal);
//递归去找两边的子树,左子树的终点就是我们的根节点,右子树的终点就是我们的整形最大值
cur.left = recursive(preorder,inorder,curVal);
cur.right = recursive(preorder,inorder,stop);
return cur;
}
}
JZ5 用两个栈实现队列
题目描述
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
解题思路
模拟即可。一个栈用来存,一个用来读。当读为空的时候,将存的出栈读的入栈,最后返回读的栈顶就是我们的队列的顺序了。
实现代码
public class Solution {
//使用Stack也行,这里我用双端队列只要不使用队列的特性本质是一样的
Deque<Integer> stack1 = new ArrayDeque<>();
Deque<Integer> stack2 = new ArrayDeque<>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if(!stack2.isEmpty()){
return stack2.pop();
}else{
//如果stack2为空的时候,将stack1出栈stack2入栈复制过来,这样就是队列顺序
if(!stack1.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
return stack2.pop();
}
}
return 0;
}
}
JZ6 旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解题思路
使用二分对我们的数组进行查找。这里注意几个判断的点即可:
- 首先进行二分,当我们的中点小于我们的右边的端点,说明右半部分是有序的,那我们的最小数字就是在左半部分(包括当前的节点),更新right。
- 如果我们的中点大于我们的右边的端点,说明左半部分是有序,那就去右半部分查找,更新left。
- 如果等于的话,我们就将右边的端点向左挪移一位,继续比较。最后返回的左端点一定是最小值。
实现代码
public class Solution {
public int minNumberInRotateArray(int [] array) {
int left = 0;
int right = array.length - 1;
while(left < right){
int mid = left + (right - left) / 2;
//中点小于右端点,说明右半部分是有序的,去左边找
if(array[mid] < array[right]){
right = mid;
}else if(array[mid] > array[right]){
//中点大于右端点,说明左半部分是有序的,去右边找
left = mid + 1;
}else{
//相等挪移右端点向左继续比较
right--;
}
}
//返回的左端点一定是最小值
return array[left];
}
}
JZ7 斐波那契数列
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。n<=39
解题思路
根据规律进行模拟即可。f(0) = 0,f(1) = 1,从第二项开始f(n) = f(n - 1) + f(n - 2)。
(1)递归,在数据大的时候容易爆栈,不建议使用。
(2)递推,模拟即可。这里可以优化一下使用滚动数组,使用O(1)的空间。
实现代码
public class Solution {
public int Fibonacci(int n) {
int a = 0, b = 1,c = 0;
if(n == 0) return a;
if(n == 1) return b;
//使用滚动数组思维优化空间
for(int i = 2;i <= n; i++){
c = a + b;
a = b;
b = c;
}
return c;
}
}
JZ8 跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
解题思路
解题方法类似上一题斐波那契数列,模拟即可。同样可以使用递归和递推,递推可以使用滚动数组的思想优化成O(1)空间。
实现代码
public class Solution {
public int JumpFloor(int target) {
int a = 1,b = 2,c = 0;
if(target <= 0) return 0;
if(target == 1) return a;
if(target == 2) return b;
for(int i = 3;i <= target;i++){
c = a + b;
a = b;
b = c;
}
return c;
}
}
JZ9 变态跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题思路
找规律,发现规律直接模拟输出即可。
- n = 1,我们有1种。
- n = 2,我们有2种,{1,1},{2}。
- n = 3,我们有4种,{1,1,1},{1,2},{2,1},{3}。
- n = ......
规律f(n) = 2^(n - 1)次方。
实现代码
public class Solution {
public int JumpFloorII(int target) {
return (int)Math.pow(2,target - 1);
}
}
JZ10 矩形覆盖
题目描述
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
比如n=3时,2*3的矩形块有3种覆盖方法:
解题思路
这一题其实和上面青蛙跳台阶和斐波那契数列是一样的,变的只是场景。我们可以把一个边长为n的边看成n - 1的长度加1和n - 2的长度加2的情况相加,也就是f(n) = f(n - 1) + f(n - 2)。
实现代码
public class Solution {
public int RectCover(int target) {
if(target == 0) return 0;
if(target == 1) return 1;
if(target == 2) return 2;
int a = 1,b = 2,c = 0;
for(int i = 3; i <= target; i++){
c = a + b;
a = b;
b = c;
}
return c;
}
}
JZ11 二进制中1的个数
题目描述
输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。
解题思路&实现代码
(1)使用Java的自带的方法将整数转换成二进制,然后进行统计。
public class Solution {
public int NumberOf1(int n) {
int temp = 0;
//将整数转换成二进制数
String s = Integer.toBinaryString(n);
for(int i = 0; i < s.length(); i++){
if(s.charAt(i) == '1'){
temp++;
}
}
return temp;
}
}
(2)直接用进行&运算,根据1 & 1 = 1进行统计结果为1的个数,然后右移继续。
以5为例,运算过程如下:
5转换成2进制与1运算
101
&001 ans += 1;
010
&001
001
&001 ans += 1;
所以最后统计的ans就是等于2
实现代🐴:
public class Solution {
public int NumberOf1(int n) {
int temp = 0;
while(n != 0){
temp += n & 1;
n >>>= 1;
}
return temp;
}
}
JZ12 数值的整数次方
题目描述
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
解题思路
这里要求我们去实现而不是用自带的库方法,具体实现考虑一下细节,优化的可以使用快速幂的思想。详解见代码注解
实现代码
public class Solution {
public double Power(double base, int exponent) {
if(base == 0) return 0;
long b = exponent;
double res = 1.0;
//如果我们的是负次方幂,那么可以直接将我们的基值变成小数,然后变成整数来运算
if(b < 0){
base = 1/base;
b = -b;
}
while(b > 0){
//这里主要运用到了快速幂的思想,具体可以以5带入算一下就理解了
if((b & 1) == 1) res *= base; //b & 1 等同于 b % 2
base *= base;
b >>= 1;
}
return res;
}
}
JZ13 调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路&实现代码
(1)使用辅助数组。用一个list第一遍遍历的时候存取奇数,第二遍遍历的时候存取偶数,最后再对原数组进行赋值。时间复杂度O(n),空间复杂度O(n)。
public class Solution {
public void reOrderArray(int [] array) {
//使用临时数组来进行存储
ArrayList<Integer> list = new ArrayList<>();
//第一遍存奇数
for(int i = 0; i < array.length; i++){
if((array[i] & 1) == 1){
list.add(array[i]);
}
}
//第二遍存偶数
for(int i = 0; i < array.length; i++){
if((array[i] & 1) != 1){
list.add(array[i]);
}
}
//最后对原数组进行变换
for(int i = 0; i < array.length; i++){
array[i] = list.get(i);
}
}
}
(2)不使用额外数组。直接对原数组进行操作,在遍历奇数的时候向前挪移到已有奇数的后一位数,然后对区间的数都向后挪移一位。时间复杂度O(n^2),空间复杂度O(1)。与上面是一种时间换空间的考量!
public class Solution {
public void reOrderArray(int [] array) {
int i = 0; //记录奇数可以存放的位置
for(int j = 0; j < array.length; j++){
if((array[j] & 1) == 1){ //为奇数时,移到已有奇数的后一位数
int temp = array[j];
//区间内都向后挪移一个位置
for(int k = j - 1; k >= i; k--){
array[k + 1] = array[k];
}
array[i++] = temp;
}
}
}
}
JZ14 链表中倒数第k个结点
题目描述
输入一个链表,输出该链表中倒数第k个结点。
解题思路&实现代码
(1)使用辅助数组。可以将数据存进list里面,然后直接用获取list.size() - k位置的结点就行。
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
//遍历链表,将数据存进list里,然后直接获取对应位置
ArrayList<ListNode> list = new ArrayList<ListNode>();
while(head != null){
// ListNode temp = new ListNode(head.val);
list.add(head);
head = head.next;
}
//边界判断
if(k > list.size() || k == 0) return null;
return list.get(list.size() - k);
}
}
(2)快慢指针。让快指针先跑k步,然后和慢指针一起跑。最后快指针到达终点的时候,慢指针就是倒数第k个结点。
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
//先判空,如果结点为空,直接返回null
if(head == null) return null;
ListNode left = head;
ListNode right = head;
//让我们的快指针先跑k步
while(k-- > 0){
//如果快指针直接跑过终点,说明慢指针没机会跑
if(right == null) return null;
right = right.next;
}
//然后一起跑,当快指针到达终点,慢指针就是倒数第k个结点
while(right != null){
right = right.next;
left = left.next;
}
return left;
}
}
JZ15 反转链表
题目描述
输入一个链表,反转链表后,输出新链表的表头。
解题思路&实现代🐴
(1)借用辅助数组,先将数据读取出来,然后再重新用一个结点逆序遍历装上去,这样就实现了链表的反转。
public class Solution {
public ListNode ReverseList(ListNode head) {
//使用辅助数组
ArrayList<Integer> list = new ArrayList<>();
ListNode pre = head;
while(pre != null){
list.add(pre.val);
pre = pre.next;
}
//用一个初始结点来保存状态,另一个结点用来遍历添加
ListNode res = new ListNode(0);
ListNode cur = res;
for(int i = list.size() - 1; i >= 0; i--){
cur.next = new ListNode(list.get(i));
cur = cur.next;
}
return res.next;
}
}
(2)不使用辅助数组,使用两个结点来反转。一个结点正常遍历,另一个结点来保存当前的结点,并将当前的结点指向上一个结点。正常结点遍历完,我们的反转后的结点就是反转链表的头结点。
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null) return null;
//两个结点,一个正常遍历,一个用来保存反转的链表
ListNode pre = head;
ListNode cur = null;
while(pre != null){
ListNode temp = new ListNode(pre.val);
temp.next = cur;
cur = temp;
pre = pre.next;
}
return cur;
}
}
JZ16 合并两个有序链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
解题思路
经典题了,两个链表合并首先应该想到的就是归并思想。我们对两个链表进行遍历,判断大小然后顺序接在合并链表的结点上。(当然下流的写法就是用辅助存一下然后再构建链表,这里就不给出代码了。)
实现代码
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
//先初始我们的根结点状态,以便返回
ListNode root = new ListNode(0);
ListNode cur = root;
//对其中较没有最大数的结点进行遍历完
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
cur.next = list1;
list1 = list1.next;
}else{
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
//如果没有遍历完的链表直接接在后面就行,因为都是排序后的链表
if(list1 != null) cur.next = list1;
else cur.next = list2;
return root.next;
}
}
JZ17 树的子结构
题目描述
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
解题思路
这里我们首先要先想一下,两个树怎么样才能算相等。
两个树相等的条件首先当前结点值相等,左右子树的结点值也相等,然后下面所有的结点都一样,才能算相等。那么回到题目,我们这里是要我们判断B是不是A的子结构,我们就可以想到将B的根结点和A的每一个结点进行比较,如果其中有一个结点开始的子树与B树相等并且下面的子树和B下面的子树也相等,我们就可以说B是A的子结构。
实现代码
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
//如果其中有一个为空的话,那么就不能是构成子结构的形式
if(root1 == null ||root2 == null) return false;
//将B树与A树每一个结点的子树进行比较,调用下面比较的方法
return equalNode(root1,root2)
|| HasSubtree(root1.left,root2)
|| HasSubtree(root1.right,root2);
}
//比较两个树的所有结点都相等,最后返回true
public boolean equalNode(TreeNode t1,TreeNode t2){
if(t2 == null) return true;
if(t1 == null) return false;
return t1.val == t2.val && equalNode(t1.left,t2.left) && equalNode(t1.right,t2.right);
}
}
JZ18 二叉树的镜像
题目描述
操作给定的二叉树,将其变换为源二叉树的镜像。
二叉树的镜像定义:源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
解题思路
模拟,对左右子树进行交换,递归交换所有子树下的左右子树。当然如果不使用递归的话,可以层次遍历每个结点然后交换结点下面的左右子结点。
实现代码
(递归)
public class Solution {
public void Mirror(TreeNode root) {
if(root == null) return;
//镜像,说白了就是对左右子树进行交换
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
//递归子树下的左右子树进行交换
Mirror(root.left);
Mirror(root.right);
}
}
(递推)
public class Solution {
public void Mirror(TreeNode root) {
if(root == null) return;
//使用队列进行层次遍历交换左右子树
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while(!q.isEmpty()){
int len = q.size();
for(int i = 0; i < q.size(); i++){
TreeNode cur = q.poll();
if(cur.left != null) q.offer(cur.left);
if(cur.right != null) q.offer(cur.right);
TreeNode temp = cur.left;
cur.left = cur.right;
cur.right = temp;
}
}
}
}
JZ19 顺时针打印矩阵
题目描述
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
矩阵如下:
[1,2,3,4]
[5,6,7,8]
[9,10,11,12]
[13,14,15,16]
解题思路
这一题和大家可能做过的螺旋矩阵是一样的题目。模拟递推,直接对周围进行遍历。判断好边界条件即可通过!(图片来自牛客官方题解)
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
int top = 0,down = matrix.length;
if(down < 1) return new ArrayList<>();
int left = 0,right = matrix[0].length;
if(right < 1) return new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
//顺时针遍历
while(top < down && left < right){
//打印上侧
for(int i = left; i < right; i++){
list.add(matrix[top][i]);
}
top++;
//打印左侧
for(int i = top; i < down; i++){
list.add(matrix[i][right - 1]);
}
right--;
//打印下侧
for(int i = right - 1; i >= left && top <= down - 1; i--){
list.add(matrix[down - 1][i]);
}
down--;
//打印右侧
for(int i = down - 1; i >= top && left <= right - 1; i--){
list.add(matrix[i][left]);
}
left++;
}
return list;
}
}
JZ20 包含min函数的栈
题目描述
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
解题思路
首先这一题可以用栈,重点在于实现O(1)的min函数。这里我们需要用到一个辅助栈,用来存每一个栈位置的下面最小的数,在出栈的时候一起出栈就行。以{4,5,3,2,6}入栈为例:
- 主栈进入4,当前最小的是4,辅助栈也进入4。
- 主栈进入5,当前辅助栈顶最小的是4,同样再次进入4。
- 主栈进入3,同时小于当前辅助栈顶,所以也进入辅助栈,此时辅助栈顶为3。
- 后面同样进入操作,最后主栈是{4,5,3,2,6},辅助栈是{4,4,3,2,2}。在我们每一个最小的值出栈后,都可以继续以O(1)获取当前的最小值,返回辅助栈顶就是了。
实现代码
public class Solution {
Stack<Integer> stack = new Stack<>();
Stack<Integer> min = new Stack<>();
//进行比较后再进入辅助栈
public void push(int node) {
stack.push(node);
if(min.isEmpty()){
min.push(node);
}else{
if(node <= min.peek()){
min.push(node);
}else{
min.push(min.peek());
}
}
}
public void pop() {
stack.pop();
min.pop();
}
public int top() {
return stack.peek();
}
public int min() {
return min.peek();
}
}
JZ21 栈的压入、弹出序列
题目描述
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
解题思路
模拟即可。用一个栈来模拟一下这个压入、弹出过程,如果最后栈不为空的话,说明这个弹出序列是不是一个合格的顺序。
实现代码
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
//解法这里可以用一个栈来模拟这个过程,只要我们最后的栈按照这个出栈序列可以全部出栈即可
int left = 0,right = 0;
Stack<Integer> st = new Stack<>();
while(left < pushA.length){
if(pushA[left] != popA[right]){
st.push(pushA[left++]);
}else{
left++;
right++;
while(!st.isEmpty() && st.peek() == popA[right]){
st.pop();
right++;
}
}
}
return st.isEmpty();
}
}
JZ22 从上往下打印二叉树
题目描述
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
解题思路
二叉树的层次遍历,队列实现模拟bfs就行。
实现代码
public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
if(root == null) return new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
//用一个队列模拟一下广度优先搜索即可打印所有的节点
while(!q.isEmpty()){
int size = q.size();
for(int i = 0; i < size; i++){
TreeNode temp = q.poll();
if(temp.left != null) q.offer(temp.left);
if(temp.right != null) q.offer(temp.right);
list.add(temp.val);
}
}
return list;
}
}
JZ23 二叉搜索树的后序遍历
题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。
解题思路
这一题写的时候没思路,参考了牛客@Jalr4ever
的题解。如下:
二叉搜索树,举例如下:
结合图中分析:
- 一棵 BST :左孩子 < 根结点 < 右孩子
- 一棵 BST 的左子树或者右子树都是 BST
后序遍历是,左右根:[3, 4, 9, 5, 12, 11, 10],结合图再从左往右分析后序序列,分析子树,可以发现:
- [3, 4, 9, 5] 10 [12, 11]
- [3, 4] 5 [9]
- [3] 4
- [12] 11
- [3, 4] 5 [9]
发现对于每一棵子树,它的根结点总是对应该子树的后序序列的最后一个数
那么,只需要不断地确定出左子树区间和右子树区间,并且判断:左子树区间的所有结点值 < 根结点值 < 右子树区间所有结点值,这个条件是否满足即可。
实现代码
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if(sequence.length < 1) return false;
return verify(sequence,0,sequence.length - 1);
}
public boolean verify(int[] sequence,int left,int right){
if(left >= right) return true;
//经过分析可以知道最后的结点一定是我们每棵子树的根结点
int root = sequence[right];
int mid = left;
//找到左右子树的分界点,即mid的最终值
for(; mid <= right && sequence[mid] < root; mid++);
//对与右子树里面,如果存在比根节点小的值说明不是二叉搜索树
for(int i = mid; i <= right; i++){
if(sequence[i] < root) return false;
}
return verify(sequence,left,mid - 1) || verify(sequence,mid,right);
}
}
JZ24 二叉树中和为某一值的路径
题目描述
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
解题思路
从根节点开始向左右子树遍历,当路径和为目标值并且到达叶子节点时候,加入集合。然后回溯继续遍历。具体的例子如下图:
实现代码
public class Solution {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
ArrayList<Integer> path = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
if(root == null) return res;
build(root,target);
return res;
}
public void build(TreeNode root,int target){
if(root == null) return;
path.add(root.val);
target -= root.val;
//当路径和为目标并且到达了叶子节点则符合要求,进行添加
if(target == 0 && root.left == null && root.right == null){
res.add(new ArrayList(path));
}
build(root.left,target);
build(root.right,target);
//回溯将之前添加的元素移除掉
path.remove(path.size() - 1);
}
}
JZ25 复杂链表的复制
题目描述
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
解题思路
这一题写的时候思路不清晰,参考自@无名者
,代码也是。总的思路就是先遍历一边把所有的结点都new一下,然后一遍再把随机结点给带上。这里用的是HashMap可以更容易操作,代码如下。
实现代码
import java.util.HashMap;
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
if (pHead == null) return null;
// target 作为将要返回的头,记住要new的
RandomListNode target = new RandomListNode(pHead.label);
// cur 获取链表头
RandomListNode cur = pHead;
// p 获取新链表头
RandomListNode p = target;
HashMap<RandomListNode, RandomListNode> map = new HashMap<>();
// 由pHead将所有值存入map,每一个结点都要new的
while (pHead != null) {
map.put(pHead, new RandomListNode(pHead.label));
pHead = pHead.next;
}
// target作为新链表的头,由cur,p移动来复制链表
while (cur != null) {
p.next = map.get( cur.next );
p.random = map.get( cur.random );
cur = cur.next;
p = p.next;
}
return target;
}
}
JZ26 二叉搜索树与双向链表
题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
解题思路
这一题我们如果分析一下就可以发现,二叉搜索树的中序遍历其实就是一个排序链表的遍历。所以我们只需要在中序遍历的时候来调整节点的指向就行。我们可以用一个空节点节点来保存第一个节点当作我们双向链表的表头。同时我们可以把左节点当作上一个节点,右节点当作下一个节点来进行修改树的结点。
实现代码
首先我们通过中序遍历就可以建立起一个双向链表,但是这样最后我们的表头是是指向链表尾的那个值,相当于如果遍历这个链表的话,就是逆序输出。所以这种写法不行!
public class Solution {
//我们的链表头
TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null) return null;
//先遍历左子树
Convert(pRootOfTree.left);
if(pre != null){
pRootOfTree.left = pre;
pre.right = pRootOfTree;
}
pre = pRootOfTree;
//再遍历右子树
Convert(pRootOfTree.right);
return pre;
}
}
我们只需要逆向思维,中序遍历(左根右是我们的逆序),那么我们就用右根左来进行遍历即可。
手绘流程图:
public class Solution {
//我们的链表头
TreeNode pre = null;
public TreeNode Convert(TreeNode pRootOfTree) {
if(pRootOfTree == null) return null;
//先遍历右子树
Convert(pRootOfTree.right);
if(pre != null){
pRootOfTree.right = pre;
pre.left = pRootOfTree;
}
pre = pRootOfTree;
//再遍历左子树
Convert(pRootOfTree.left);
return pre;
}
}
JZ27 字符串的排列
题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则按字典序打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
解题思路
全排列问题,搜索+回溯。引用牛客的一张图解析:
我们只要对字符串里面的每一个字符进行与后面的交换,就可以达到一个排列的效果。但是这里需要回溯,在上面的图中我们可以看到,比如ABC与ACB这两个分支,我们都要回到ABC这里来进行交换,所以就需要回溯上一个状态。具体实现代码如下,代码可能过于繁琐,有点丑陋......
实现代码
public class Solution {
//定义返回结果的list
ArrayList<String> res = new ArrayList<>();
//用来辅助达到去重效果
Set<String> set = new HashSet<>();
public ArrayList<String> Permutation(String str) {
ArrayList<Character> list = new ArrayList<>();
if(str.length() < 1) return res;
for(int i = 0; i < str.length(); i++) list.add(str.charAt(i));
dfsString(0,list);
//不知道为什么我递归的不是按照字典序,只能手动再排一下序
Collections.sort(res);
return res;
}
public void dfsString(int index,ArrayList<Character> list){
if(index == list.size() - 1){
//如果我们的下标到达了最后一个说明已经交换过了一轮,可以添加
StringBuilder sb = new StringBuilder();
for(Character c : list) sb.append(c);
if(set.contains(sb.toString())) return;
res.add(sb.toString());
set.add(sb.toString());
return;
}
// for循环和swap的含义:对于“ABC”,
// 第一次'A' 与 'A'交换,字符串为"ABC", index为0, 相当于固定'A'
// 第二次'A' 与 'B'交换,字符串为"BAC", index为0, 相当于固定'B'
// 第三次'A' 与 'C'交换,字符串为"CBA", index为0, 相当于固定'C'
for(int i = index; i < list.size(); i++){
Collections.swap(list,index,i);
dfsString(index + 1,list);
Collections.swap(list,index,i);
}
}
}
JZ28 数组中出现次数超过一半的数字
题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路&实现代码
(1)模拟,首先呢就是可以Map来存取我们的数组的值为key,出现的次数为具体value。只要判断我们的value大于我们的数组长度的一半的时候就返回key(最开始的想到解法)。时间复杂度为O(n),空间复杂度为O(n)。
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
Map<Integer,Integer> map = new HashMap<>();
int mid = array.length / 2;
for(int i = 0; i < array.length; i++){
map.put(array[i],map.getOrDefault(array[i],0) + 1);
if(map.get(array[i]) > mid) return array[i];
}
return 0;
}
}
(2)将数组排序,然后根据众数在中间的特性,判断并统计。因为时间复杂度达到O(nlogn),不可取。
(3)牛客网的参考答案(最优解)。利用众数的特性,如果众数大于数组长度的一半,那么用众数与其他数相抵消最后一定剩下的是众数。遍历一下数组统计一下这个众数的是否大于数组长度的一半返回结果。时间复杂度为O(n),空间复杂度为O(1)。
具体做法:
- 初始化:候选人cond = -1, 候选人的投票次数cnt = 0
- 遍历数组,如果cnt=0, 表示没有候选人,则选取当前数为候选人,++cnt
- 否则,如果cnt > 0, 表示有候选人,如果当前数=cond,则++cnt,否则--cnt
- 直到数组遍历完毕,最后检查cond是否为众数
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
int cond = -1;
int cnt = 0;
for(int i = 0; i < array.length; i++){
if(cnt == 0){
cond = array[i];
cnt++;
}else{
if(cond == array[i]) cnt++;
else cnt--;
}
}
cnt = 0;
for(int i = 0; i < array.length; i++){
if(cond == array[i]) cnt++;
}
return cnt > array.length / 2 ? cond : 0;
}
}
JZ29 最小的K个数
题目描述
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
解题思路
(1)对数组进行排序,然后遍历到k就行。代码简单不实现了。时间复杂度为O(nlogn)。
(2)可以维护一个长度为k的一个大根堆,堆没满的时候,直接添加进去就行;如果堆满了,每次添加进行判断,如果添加的数大于堆顶则不进行添加,如果小于则进行添加并置换出堆顶。最后把堆输出到我们要求的list中返回就行。时间复杂度为O(nlogk)。当然如果数据量在特别小的时候是不如上面的直接排序的。这里如果可以的话,最好自己手动实现一个大根堆!
代码实现
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
if(k > input.length || k < 1) return new ArrayList<>();
PriorityQueue<Integer> pq = new PriorityQueue<>((o1, o2) -> o2 - o1); //默认小根堆,我们要改成大根堆
for(int i = 0; i < input.length; i++){
if(pq.size() < k){
pq.offer(input[i]);
}else{
if(input[i] < pq.peek()){
pq.poll();
pq.offer(input[i]);
}
}
}
ArrayList<Integer> list = new ArrayList<>(pq);
return list;
}
}
JZ30 连续子数组的最大和
问题描述
HZ(皇杂)偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
解题思路
动态规划,如果添加当前值后小于当前值,说明不值得添加,当前值重新开始,反之则继续添加。
实现代码
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int[] dp = new int[array.length];
dp[0] = array[0];
int res = Integer.MIN_VALUE;
for(int i = 1; i < dp.length; i++){
//对每一个dp位置的值进行初始化
dp[i] = array[i];
//如果当前添加前面小于原来的值的话,那么不进行添加
dp[i] = Math.max(dp[i],dp[i - 1] + array[i]);
res = Math.max(dp[i],res);
}
return res;
}
}
(优化)上面我们开辟O(n)的空间,这里其实还可以用滚动数组的思想进行优化一下
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int res = Integer.MIN_VALUE;
int ans = 0;
for(int i = 0; i < array.length; i++){
//利用滚动数组的思想优化
ans = Math.max(array[i],ans + array[i]);
res = Math.max(ans,res);
}
return res;
}
}
JZ31 整数中1出现的次数
问题描述
求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
解题思路&实现代码
(1)模拟。直接将每一个数字转换成字符串然后遍历进行统计。缺点就是复杂过大
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
int count = 0;
for(int i = 1; i <= n; i++){
String str = String.valueOf(i);
for(int j = 0; j < str.length(); j++){
if(str.charAt(j) == '1') count++;
}
}
return count;
}
}
(2)找规律。思路来自牛客@王小明1696
。
思路是分别计算个位、十位、百位........上出现 1 的个数。
以 n =216为例:
个位上: 1 ,11,21,31,.....211。个位上共出现(216/10)+ 1个 1 。因为除法取整,210~216间个位上的1取不到,所以我们加8进位。你可能说为什么不加9,n=211怎么办,这里把最后取到的个位数为1的单独考虑,先往下看。
十位上:1019,110119,210~216. 十位上可看成 求(216/10)=21 个位上的1的个数然后乘10。这里再次把最后取到的十位数为1的单独拿出来,即210~216要单独考虑 ,个数为(216%10)+1 .这里加8就避免了判断的过程。
后面以此类推。
时间复杂度 O(logN)
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
int count = 0;
for(int i = 1; i <= n; i *= 10){
int a = n / i, b = n % i;
count += (a + 8) / 10 * i + (a % 10 == 1 ? b + 1 : 0);
}
return count;
}
}
JZ32 把数组排成最小的数
问题描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路&实现代码
我们将数组里面的数字当作字符串进行字典序排序就行,然后从小到大字符串拼接即可。
import java.util.ArrayList;
public class Solution {
public String PrintMinNumber(int [] numbers){
//对数组进行字典序排序
ArrayList<String> list = new ArrayList<>();
for(int i = 0; i < numbers.length; i++){
list.add(String.valueOf(numbers[i]));
}
list.sort((o1, o2) -> {
String temp = o1 + o2;
return temp.compareTo(o2 + o1);
});
StringBuilder sb = new StringBuilder();
for(String i : list){
sb.append(i);
}
return sb.toString();
}
}
JZ33 丑数
问题描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路
题目首先说到了1是第一个丑数。我们要找出后面的丑数,需要对三个因子进行相乘便可以找到。第一组1*[2,3,5] = [2,3,5],第二组2 * [2,3,5] = [4,6,5],后面类似。但是因为我们要的是重复有序的,所以不能直接相乘存进我们的数组里面去,而是要通过比较,一个个存进去,具体实现参考代码。
实现代码
public class Solution {
public int GetUglyNumber_Solution(int index) {
//定义三个指针,进行因子迭代
int p2 = 0,p3 = 0,p5 = 0;
if(index < 1) return 0;
int[] arr = new int[index];
arr[0] = 1; //把1当作第一个丑数
for(int i = 1; i < index; i++){
//每次只选取三个数中最小的那一个数
arr[i] = Math.min(arr[p2] * 2,Math.min(arr[p3] * 3,arr[p5] * 5));
//对选中的数进行更新
if(arr[i] == arr[p2] * 2) p2++;
if(arr[i] == arr[p3] * 3) p3++;
if(arr[i] == arr[p5] * 5) p5++;
}
return arr[index - 1];
}
}
JZ34 第一个只出现的字符
题目描述
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
解题思路
按照题目要求模拟,用哈希key的唯一性来判断我们的数据进行存。然后再一次遍历可以判断第一个唯一返回坐标即可。
实现代码
public class Solution {
public int FirstNotRepeatingChar(String str) {
//借用辅助数组,然后使用map的key唯一性,来判断我们的是否只出现了一次
HashMap<Character,Integer> map = new HashMap<>();
for(int i = 0; i < str.length(); i++){
map.put(str.charAt(i),map.getOrDefault(str.charAt(i),0) + 1);
}
for(int i = 0; i < str.length(); i++){
if(map.get(str.charAt(i)) == 1){
return i;
}
}
return -1;
}
}
JZ35 数组中的逆序对
题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
题目保证输入的数组中没有的相同的数字数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
如输入1,2,3,4,5,6,7,0
输出7
解题思路
这一题的解题思想很好理解,但是要动手实现有点复杂。解题思路就是运用到了归并的思想。我们在归并排序中,会将一个数组分割到最后只剩下两个数,进行比较之后开始进行合并。于是我们在合并的时候就可以进行统计。如果两个合并区间[1]和[3],3在前面,那么count += 1;如果区间3后面还有很多数之后,至少可以保证,这个区间都是大于[1]所在的区间,所以就可以count += 3区间的长度。
实现代码
直接基于归并排序模版上面修改的。
public class Solution {
public int InversePairs(int [] array) {
int len = array.length;
if(len < 2) return 0;
int[] temp = new int[len];
for(int i = 0; i < len; i++){
temp[i] = array[i];
}
int[] help = new int[len];
return mergeSort(temp,0,len-1,help) % 1000000007;
}
public int mergeSort(int[] nums,int left,int right,int[] help){
if(left == right) return 0;
int mid = left + (right - left) / 2;
long leftPair = mergeSort(nums,left,mid,help);
long rightPair = mergeSort(nums,mid+1,right,help);
if(nums[mid] <= nums[mid+1]) return (int)(leftPair+rightPair)% 1000000007 ;
long pairCount = merge(nums,left,mid,right,help);
return (int)(leftPair + rightPair + pairCount) % 1000000007;
}
public long merge(int[] nums,int left,int mid,int right,int[] help){
for(int i = left; i <= right; i++){
help[i] = nums[i];
}
//用来统计归并过程中逆序对的数量
long count = 0;
int i = left,j = mid+1;
for(int k = left; k <= right; k++){
if(i == mid+1){
nums[k] = help[j];
j++;
}else if(j == right+1){
nums[k] = help[i];
i++;
}else if(help[i] <= help[j]){
nums[k] = help[i];
i++;
}else{
nums[k] = help[j];
j++;
count += (mid - i + 1);
}
}
return count % 1000000007;
}
}
JZ36 两个链表的第一个公共结点
题目描述
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
解题思路&实现代码
(1)借用辅助数组。我们可以用set对第一个链表进行遍历并将它的每一个结点都存下来,然后再去遍历第二个链表,如果在遍历期间出现重复的结点,就可以说明这就是我们的公共结点。
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
//意思就是找到两个链表的交点
//最简单的方法就是我们可以借用辅助数组
Set<ListNode> set = new HashSet<>();
while(pHead1 != null){
set.add(pHead1);
pHead1 = pHead1.next;
}
while(pHead2 != null){
if(set.contains(pHead2)){
return pHead2;
}
pHead2 = pHead2.next;
}
return null;
}
}
(2)不借用辅助数组。创建两个指针p1和p2,分别指向两个链表的头结点,然后依次往后遍历。如果某个指针到达末尾,则将该指针指向另一个链表的头结点;如果两个指针所指的节点相同,则循环结束,返回当前指针指向的节点。我们可以想到,如果p1是1-3-5,p2是2-4-7-3-8。他们的第一个公共结点应该是3,但是因为我们p1遍历了两次就到了,而p2要遍历4次才能到。所以我们要做的就是让他们的遍历的长度相等,也就是加上对方的长度,这样就可以保证遍历第一个公共结点的长度是相等。
1-3-5-2-4-7-3
-8
2-4-7-3-8-1-3
-5
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1 == null || pHead2 == null) return null;
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while(p1 != p2){
p1 = p1 == null ? pHead2 : p1.next;
p2 = p2 == null ? pHead1 : p2.next;
}
return p1;
}
}
JZ37 数字在升序数组中出现的次数
题目描述
统计一个数字在升序数组中出现的次数。
解题思路
首先使用二分对我们的数组进行k的查找,如果找到则进行两边与k等值的数遍历统计,最后返回我们的统计;如果没有找到直接返回我们的0即可。
实现代码
public class Solution {
public int GetNumberOfK(int [] array , int k) {
int left = 0,right = array.length - 1;
int count = 0;
while(left <= right){
int mid = left + (right - left) / 2;
if(array[mid] == k){
//通过二分查找到k之后,对该坐标两边进行查找,进行统计
count += 1;
for(int i = mid - 1; i >= 0 && array[i] == k; i--) count++;
for(int i = mid + 1; i <= array.length - 1 && array[i] == k; i++) count++;
return count;
}else if(array[mid] > k){
right = mid - 1;
}else{
left = mid + 1;
}
}
return count;
}
}
JZ38 二叉树的深度
题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题思路
递归。我们一颗二叉树的深度可以看成是左右子树中深度最大的加1,根据这个结论递归即可。
实现代码
public class Solution {
public int TreeDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(TreeDepth(root.left),TreeDepth(root.right)) + 1;
}
}
JZ39 平衡二叉树
题目描述
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
在这里,我们只需要考虑其平衡性,不需要考虑其是不是排序二叉树
解题思路
要判断该二叉树是否是平衡二叉树,我们首先要知道什么是平衡二叉树。所谓平衡二叉树,即任意节点的子树的高度差都小于等于1。那么我们只需要遍历对我们每一个子节点进行判左右子树即可。
这一题实现可以运用到上面的求深度。
实现代码
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
if(root == null) return true;
//对左右子树递归判断
boolean leftTree = IsBalanced_Solution(root.left);
boolean rightTree = IsBalanced_Solution(root.right);
//如果左右子树的高度差大于1,说明不平衡
return (Math.abs(getMaxDepth(root.left) - getMaxDepth(root.right)) <= 1);
}
//求子树的最大深度
public int getMaxDepth(TreeNode root){
if(root == null) return 0;
return Math.max(getMaxDepth(root.left),getMaxDepth(root.right)) + 1;
}
}
JZ40 数组中只出现一次的数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
解题思路
(点击跳转)
实现代码
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
//第一遍遍历我们对数组进行^操作,那么最后的结果就是num1[0]^num2[0]
int ans = 0;
for(int i = 0; i < array.length; i++){
ans ^= array[i];
}
int lowbit = ans & (-ans);
for(int i : array){
if((i & lowbit) == 0){
num1[0] ^= i;
}else{
num2[0] ^= i;
}
}
return;
}
}
JZ41 和为S的连续正数序列
题目描述
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
解题思路
这一题没什么特别好的思路,只要暴力模拟即可。
实现代码
public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> lists = new ArrayList<>();
//这里我们简单优化一下,因为是连续的序列,所以是不可能超过我们的一半的数
for(int i = 1; i <= sum / 2; i++){
ArrayList<Integer> list = new ArrayList<>();
int ans = 0;
for(int j = i; j < sum; j++){
list.add(j);
ans += j;
if(ans == sum){
//序列之和等于我们的sum的话,那么直接添加到我们的列表集合里面
lists.add(list);
break;
}
if(ans > sum) break;
}
}
return lists;
}
}
JZ42 和为S的两个数字
问题描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
对应每个测试案例,输出两个数,小的先输出。
解题思路&实现代码
(1)使用我们的哈希表,将每一个数都用sum减去,如果减去之后的数在哈希表里面存在说明找到了一对和为sum的数。同时还可以进行判断乘积是否小于已有和为sum的两个数,如果是进行更新。时间复杂度是O(n),空间复杂度O(n)。
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> list = new ArrayList<>();
//可以使用map存取我们的每一个数,如果后面出现我们和为sum的,进行乘积判断即可
HashMap<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < array.length; i++){
int temp = sum - array[i];
if(map.containsKey(temp)){ //第一次找到和为sum的两个数
if(list.size() < 1){
list.add(temp);
list.add(array[i]);
}else{ //如果list不为空的话,进行乘积比较更新
int ans = list.get(0) * list.get(1);
int max = temp * array[i];
if(max < ans){
list.clear(); //清空重新添加
list.add(temp);
list.add(array[i]);
}
}
}else{ //如果map不包含现在这个数的话,进行添加。
map.put(array[i],0);
}
}
Collections.sort(list); //按照题目要求小的先输出
return list;
}
}
(2)使用双指针。根据有序这个特点我们可以定义两个指针分别指向头尾,每次进行判断,如果和大于sum的话,右指针向左移动,和小于sum的话,左指针向右移动。如果刚好等于的话,进行乘积判断,如果乘积小于我们的已有的两个和为sum的数,那么进行更新。
import java.util.*;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> list = new ArrayList<>();
//定义双指针分别指向头和尾
int left = 0,right = array.length - 1;
//定义乘积最小的和为sum的两个数
int num1 = 0,num2 = 0;
//定义一个最小的乘积
int minAns = Integer.MAX_VALUE;
while(left < right){
int ans = array[left] + array[right];
if(ans == sum){
if(array[left] * array[right] < minAns){
num1 = array[left];
num2 = array[right];
minAns = array[left] * array[right];
}
left++; //更新完进行一次进位
}else if(ans > sum){
right--;
}else{
left++;
}
}
if(num1 != 0) list.add(num1);
if(num2 != 0) list.add(num2);
return list;
}
}
JZ43 左旋转字符串
题目描述
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
解题思路
模拟操作即可。
实现代码
public class Solution {
public String LeftRotateString(String str,int n) {
// 对字符串进行获取添加,模拟操作即可
if(str == null || str.length() < 1) return str;
//可以将n对字符串的长度进行取余,因为循环一个字符串的长度就是原来的字符串
n = n % str.length();
String left = str.substring(0,n);
String right = str.substring(n);
return right + left; //将位移后的字符串返回
}
}
JZ44 翻转单词顺序列
题目描述
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
解题思路&实现代码
(1)用String将字符串分割成一个字符串数组,然后进行拼接即可。
public class Solution {
public String ReverseSentence(String str) {
//对字符串进行分割,然后拼接
String[] strs = str.split(" ");
for(int i = 0; i < strs.length / 2; i++){
String temp = strs[i];
strs[i] = strs[strs.length - i - 1];
strs[strs.length - i - 1] = temp;
}
if(strs.length < 1) return str;
StringBuilder sb = new StringBuilder();
for(int i = 0; i < strs.length; i++){
sb.append(strs[i]);
if(i < strs.length - 1) sb.append(" ");
}
return sb.toString();
}
}
(2)剑指offer的提供的思路,先将字符串进行翻转,然后再对我们里面的每一个单词进行翻转。
public class Solution {
public String ReverseSentence(String str) {
if(str == null || str.length() < 1) return str;
char[] cs = str.toCharArray();
//先将整个字符串进行翻转
reverseStr(cs,0,str.length() - 1);
int left = 0,right = 0;
//使用双指针对,在分别指向每一个完整的单词的头尾的时候进行翻转
while(left < str.length()){
if(cs[right] == ' '){
reverseStr(cs,left,right - 1);
left = right + 1; //跳到下一个单词的开始位置
right++;
}
if(right == str.length() - 1){
reverseStr(cs,left,right);
break; //交换就break,不然陷入死循环
}
right++;
}
return String.valueOf(cs);
}
//对一个字符串进行翻转
public void reverseStr(char[] chars,int left,int right){
while(left < right){
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
return;
}
}
JZ45 扑克顺子
问题描述
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
解题思路
照着过程模拟即可。具体的要求和判定都在代码的注释里面。
补充:
还有一种思路就是用哈希,如果出现重复的返回false,其他的操作就是找最大和最小,如果最大和最小的差距大于5的话,说明是不可能组成顺子的。
实现代码
public class Solution {
public boolean isContinuous(int [] numbers) {
if(numbers.length < 1) return false;
Arrays.sort(numbers);
boolean flag = true;
int count = 0;
int ans = 0;
for(int i = 0; i < numbers.length; ){
if(numbers[i] == 0){ //统计大小王的数量
count++;
i++;
}else{
if(ans == 0){ //将第一个顺子的开头赋给ans
ans = numbers[i++];
}else{
//如果顺子断了,有大小王的话可以接上
if(numbers[i] - ans > 1 && count > 0){
count--;
ans++;
}else if(numbers[i]- ans > 1 && count == 0){
//顺子断了,没有大小王接不上,跳出循环
flag = false;
break;
}else if(ans == numbers[i]){
//遇到重复的数,组不成顺子,跳出循环
flag = false;
break;
}else{
//是顺子的话正常遍历即可
ans = numbers[i++];
}
}
}
}
return flag;
}
}
JZ46 孩子们的游戏
问题描述
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
解题思路
有些题表面上题目这么长,暗地里却是一个约瑟夫环。按照约瑟夫环的思想模拟即可。
实现代码
public class Solution {
//使用迭代
public int LastRemaining_Solution(int n, int m) {
if(n < 1) return -1;
int res = 0;
for(int i = 1; i <= n; i++){
res = (res + m) % i;
}
return res;
}
}
JZ47 求1+2+3+...+n
题目描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
解题思路
利用短路思想。具体解析跳转
实现代码
public class Solution {
public int Sum_Solution(int n) {
//利用短路原理
boolean flag = n > 0 && (n += Sum_Solution(n - 1)) >0;
return n;
}
}
JZ48 不用加减乘除做加法
题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路
本题思路摘录自牛客@箫筱沐羽
。
首先看十进制是如何做的: 5+7=12,三步走
第一步:相加各位的值,不算进位,得到2。
第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。
同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。
第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。
第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
实现代码
public class Solution {
public int Add(int num1,int num2) {
while(num2 != 0){
int temp = num1 ^ num2;
num2 = (num1 & num2) << 1;
num1 = temp;
}
return num1;
}
}
JZ49 把字符串转换成整数
题目描述
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
输入
+2147483647 1a33
输出
2147483647 0
解题思路
这一题的主要思路就是对字符串进行相加乘10进位处理,处理一下边界和正负号的问题就可以了。所以模拟的时候稍微注意一下都可以过的。
实现代码
public class Solution {
public int StrToInt(String str) {
long res = 0;
int len = str.length();
if(len < 1) return 0;
boolean flag = false; //用来标记是否这个整数是否是负数
for(int i = 0; i <= len - 1; i++){
if(Character.isDigit(str.charAt(i))){
//对与数字进行相加乘10进位处理
res = (res + (str.charAt(i) - '0'));
if(i < len - 1) res *= 10;
}else if(i == 0 && (str.charAt(i) == '+' || str.charAt(i) == '-')){
if(str.charAt(0) == '-') flag = true;
}else{
return 0;
}
}
if(flag) res = -res;
//边界处理,不能越过整型
if(res > Integer.MAX_VALUE || res < Integer.MIN_VALUE) return 0;
else return (int) res;
}
}
JZ50 数组中重复的数字
题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题思路&实现代码
(1)使用额外数组。可以使用set进行判重,在出现第一个重复的数字进行返回即可。
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(length < 1) return false;
//使用set来进行判断
//我们对数进行异或,当遇到重复的数异或之后就会出现之前出现的数
Set<Integer> set = new HashSet<>();
int ans = 0;
for(int i = 0; i < length; i++){
if(set.contains(numbers[i])){
duplication[0] = numbers[i];
return true;
}else{
set.add(numbers[i]);
}
}
return false;
}
}
(2)不使用额外数组。利用题目中给到的数据的范围是0-n-1的特性,我们对下标i与数据numbers[i]进行比较,如果相等,说明放在正确的位置,让i++;如果不相等,说明没有放在正确的位置上,那么我们就把numbers[i]与numbers[numbers[i]]进行交换,直到放在正确的位置上。如果在比较的过程中遇到numbers[i]与numbers[numbers[i]],说明遇到了第一个重复的数,那么我们就可以返回该数。
public class Solution {
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(length < 1) return false;
for(int i = 0; i < length; i++){
while(i != numbers[i]){
if(numbers[numbers[i]] != numbers[i]){
//如果无法找到的话,那么就一直进行交换。
int temp = numbers[i];
numbers[i] = numbers[numbers[i]];
numbers[temp] = temp;
}else{
duplication[0] = numbers[i];
return true;
}
}
}
return false;
}
}
JZ51 构建乘积数组
题目描述
给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]A[1]...A[i-1]A[i+1]...A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * ... * A[n-1],B[n-1] = A[0] * A[1] * ... * A[n-2];)
对于A长度为1的情况,B无意义,故而无法构建,因此该情况不会存在。
解题思路
两遍遍历,分别对数组从左和从右递增相乘。具体如图:
实现代码
public class Solution {
public int[] multiply(int[] A) {
if(A.length <= 1) return new int[]{};
int[] B = new int[A.length];
B[0] = 1;
//第一遍遍历
for(int i = 1; i < A.length; i++){
B[i] = B[i - 1] * A[i - 1];
}
int temp = 1;
//第二遍遍历
for(int i = A.length - 2; i >= 0; i--){
temp *= A[i + 1];
B[i] *= temp;
}
return B;
}
}
JZ52 正则表达式匹配
题目描述
请实现一个函数用来匹配包括'.'和''的正则表达式。模式中的字符'.'表示任意一个字符,而''表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
解题思路
可以使用递归或者动态规划的方式来实现。下面代码会以动态规划的解题思路来写,思路看注释。
实现代码
public class Solution {
public boolean match(char[] A, char[] B){
int n = A.length;
int m = B.length;
boolean[][] f = new boolean[n + 1][m + 1];
if(n==0 && m==0) return true;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
//分成空正则和非空正则两种
if (j == 0) {
f[i][j] = i == 0;
} else {
//非空正则分为两种情况 * 和 非*
if (B[j - 1] != '*') {
if (i > 0 && (A[i - 1] == B[j - 1] || B[j - 1] == '.')) {
f[i][j] = f[i - 1][j - 1];
}
} else {
//碰到 * 了,分为看和不看两种情况
//不看,跳过这个*和前面的字符看是否匹配
if (j >= 2) {
f[i][j] |= f[i][j - 2];
}
//看
//这里要用 | 连接,不然重复0次的会直接覆盖
if (i >= 1 && j >= 2 && (A[i - 1] == B[j-2] || B[j-2] == '.')) {
f[i][j] |= f[i - 1][j];
}
}
}
}
}
return f[n][m];
}
}
JZ53 表示数值的字符串
题目描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
解题思路
模拟,就是边界条件考虑的有点多。
(1) +-号后面必定为数字或后面为.(-.123 = -0.123)
(2) +-号只出现在第一位或在eE的后一位
(3) .后面必定为数字或为最后一位(233. = 233.0)
(4) eE后面必定为数字或+-号
(5) 出现非E和e的字符肯定不是数字
(6) 小数点只能有一个,并且不能在e或E后面
(7) 正常数字不管,然后其他另类全返回false就行
实现代码
public class Solution {
/*
(1) +-号后面必定为数字或后面为.(-.123 = -0.123)
(2) +-号只出现在第一位或在eE的后一位
(3) .后面必定为数字或为最后一位(233. = 233.0)
(4) eE后面必定为数字或+-号
(5) 出现非E和e的字符肯定不是数字
(6) 小数点只能有一个,并且不能在e或E后面
(7) 正常数字不管,然后其他另类全返回false就行
*/
public boolean isNumeric(char[] str) {
//根据上面的要求进行模拟判断
int len = str.length;
if(len < 1) return false;
int count = 0; //统计小数点数量
boolean flag = false; //标记e或E出现没
for(int i = 0; i < len; i++){
if(str[i] == '+' || str[i] == '-'){
//对第一种情况取反
if(i >= len - 1 || (!Character.isDigit(str[i + 1]) && str[i + 1] != '.')){
return false;
}
//对第二种情况取反
if(i > 0 && (str[i - 1] != 'E' && str[i - 1] != 'e')){
return false;
}
}else if(str[i] == '.'){
count++;
if(count > 1 || flag) return false;
//对第三种情况取反
if(i >= len - 1 || !Character.isDigit(str[i + 1])) return false;
}else if(str[i] == 'E' || str[i] == 'e'){
flag = true; //标记出现过
//对第四种情况取反
if(i >= len - 1){
return false;
}else{
if(str[i + 1] != '+' && str[i + 1] != '-'
&& !Character.isDigit(str[i + 1])) return false;
}
}else if(Character.isLetter((str[i])) && str[i] != 'e' && str[i] != 'E'){
return false;
}else if(Character.isDigit(str[i])){
}else{
return false;
}
}
return true;
}
}
JZ54 字符流中第一个不重复的字符
题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述
如果当前字符流没有存在出现一次的字符,返回#字符。
解题思路
可以使用哈希的数据结构来存取我们的字符,对与重复的字符可以对值进行统计或者标记都行。最后遍历一下哈希表就行,如果是Java的话,这里要用LinkedHashMap,因为题目要求到了要出现的第一个不重复的字符,所以如果不使用有序map的话,那么我们就不能保证取到的是第一个不重复的字符。
实现代码
public class Solution {
//Insert one char from stringstream
//因为后面要遍历保证有序,所以这里使用LinkedHashMap
Map<Character,Integer> map = new LinkedHashMap<>();
public void Insert(char ch)
{
if(map.containsKey(ch)){
map.put(ch,-1);
}else{
map.put(ch,1);
}
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
for(Character i : map.keySet()){
if(map.get(i) == 1){
return i;
}
}
return '#';
}
}
JZ55 链表环的入口结点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
解题思路
(1)最容易想到的,可以使用set进行判重。当出现重复结点的时候,那么那个结点就是我们的重复结点。时间复杂度不高,但是借用了额外的数组,所以应该不在这一题的考点里,这里也不代码编写了。
(2)不借用辅助数组。如果不使用辅助数组的话,那么就需要用到了快慢指针。我们可以用快慢指针判断一个链表中是否有环的存在,同时也可以找到环的入口结点。我们下面来分析一下:
(1)因为我们的快指针走的路程是慢指针的两倍,所以在快指针到底环的入口点的时候,假设路程为n,慢指针才走了n/2的路程。
(2)当我们的慢指针到达了环的入口结点时候,我们的快指针已经在环里面走了n的路程。假设这个环足够大,快指针还有b的路程走完就一个环了。那么当我们慢指针走了b的路程的时候,那么快指针已经走了2b的路程,刚好和快指针相遇。
(3)而且我们可以发现,慢指针还有n的路程没走,与我们头结点到环入口结点的距离相等。同样的,哪怕环小一点的话,我们的相当于分割成了多个小环来计算,最后快慢指针相遇的时候,慢指针到环的入口结点距离和头节点到环的入口结点距离相等。
(4)所以,我们最后在快慢指针相遇之后,将快指针放在头结点,让它正常行走与慢指针相遇的结点就是我们的环的入口结点。
实现代码
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead)
{
ListNode fast = pHead;
ListNode slow = pHead;
while(true){
//如果不存在环,则直接返回null
if(fast == null || fast.next == null) return null;
fast = fast.next.next; //快指针走两步
slow = slow.next; //慢指针走一步
if(fast == slow) break;
}
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
JZ56 删除链表中重复的结点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
解题思路
(1)使用set去重。不是考点不说了
(2)直接删除, 也就是不借助任何额外的空间。需要使用两个指针来进行控制链表删除。一个在指针在遇到重复的结点的时候就一直遍历,然后不重复的结点赋值给另一个指针,完成删除操作。
实现代码
public class Solution {
public ListNode deleteDuplication(ListNode pHead)
{
//遍历链表,直接删除
if(pHead == null || pHead.next == null) return pHead;
ListNode head = new ListNode(0);
head.next = pHead;
ListNode cur = head.next;
ListNode pre = head;
while(cur != null){
//将重复的结点都遍历过,然后将后面节点复制给pre结点后面
if(cur.next != null && cur.val == cur.next.val){
while(cur.next != null && cur.val == cur.next.val){
cur = cur.next;
}
pre.next = cur.next;
cur = cur.next;
}else{
pre = pre.next;
cur = cur.next;
}
}
return head.next;
}
}
JZ57 二叉树的下一个结点
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
解题思路
根据图和中序遍历的特性其实可以找到规律。
(1)当右子树不为空的时候,那么下一个结点必然是右子树下的最左子结点。
(2)当右子树为空的时候,下一个结点就是出现的第一个右父结点,比如5就是6,4就是5(如图)。
(7)否则就为7这种null。
实现代码
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode == null) return null;
//如果存在右子树,下一个结点必为右子树的最左结点
if(pNode.right != null){
pNode = pNode.right;
while(pNode.left != null){
pNode = pNode.left;
}
return pNode;
}
//不存在右子树,那么结点必然是该结点向上找的第一个右父结点。
while(pNode.next != null){
TreeLinkNode root = pNode.next;
if(root.left == pNode){
return root;
}
pNode = pNode.next;
}
return null;
}
}
JZ58 对称二叉树
题目描述
请实现一个函数,用来判断一棵二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
解题思路
根据给出的题目,我们首先要知道什么是对称二叉树。可以参考下图
我们首先可以判断第二层的两个2,他们相等是对称的,而下面要对称,那么要其中一个右子结点和另一个左子结点相等,左子结点和右子结点相等。根据这一特性,我们就可以将它递归下去判断。
public class Solution {
public boolean isSymmetrical(TreeNode root) {
if(root == null){
return true;
}
if(leftEqualRight(root.left,root.right)){
return true;
}else{
return false;
}
}
boolean leftEqualRight(TreeNode left,TreeNode right){
if(left == null && right == null){
return true;
}
if(left == null || right == null){
return false;
}
if(left.val == right.val && leftEqualRight(left.left,right.right) && leftEqualRight(left.right,right.left)){
return true;
}
return false;
}
}
JZ59 按之字形顺序打印二叉树
题目描述
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解题思路
按照层次遍历的思想进行遍历,然后可以设置一个标识量标记这次是正序打印还是逆序打印,如果是逆序打印就对链表进行反转,并且设置下次为正序打印。
实现代码
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> lists = new ArrayList<>();
if(pRoot == null) return lists;
Deque<TreeNode> q = new LinkedList<>(); //对树进行层次遍历
//可以设置一个标识量来表示这一次该从哪个顺序开始打印
int flag = 1; //为1的时候顺序打印,为0的时候逆序打印
q.offer(pRoot);
while(!q.isEmpty()){
int size = q.size();
ArrayList<Integer> list = new ArrayList<>();
for(int i = 0; i < size; i++){
TreeNode temp = q.poll();
list.add(temp.val);
if(temp.left != null) q.offer(temp.left);
if(temp.right != null) q.offer(temp.right);
}
if(flag == 1){
//正序打印不做处理
//修改下次为逆序打印
flag = 0;
}else{
//逆序打印,反转list
//修改下次为正序打印
Collections.reverse(list);
flag = 1;
}
lists.add(list);
}
return lists;
}
}
JZ60 把二叉树打印成多行
题目描述
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
解题思路
上一题的缩减版。层次打印二叉树没什么好说的。
实现代码
public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
//层次打印遍历树,没什么好说的
ArrayList<ArrayList<Integer> > lists = new ArrayList<>();
if(pRoot == null) return lists;
Queue<TreeNode> q = new LinkedList<>();
q.offer(pRoot);
while(!q.isEmpty()){
int size = q.size();
ArrayList<Integer> list = new ArrayList<>();
for(int i = 0; i < size; i++){
TreeNode temp = q.poll();
list.add(temp.val);
if(temp.left != null) q.offer(temp.left);
if(temp.right != null) q.offer(temp.right);
}
lists.add(list);
}
return lists;
}
}
JZ61 序列化二叉树
题目描述
请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
例如,我们可以把一个只有根节点为1的二叉树序列化为"1,",然后通过自己的函数来解析回这个二叉树
解题思路&解题思路
(1)钻一下空子,活跃一下气氛。😂
public class Solution {
TreeNode node;
String str = "";
String Serialize(TreeNode root) {
node = root;
return str;
}
TreeNode Deserialize(String str) {
str = new String(str);
return node;
}
}
(2)好了,这一题正常的解题思路可以看下面代码注释。写的也比较详细了
public class Solution {
String Serialize(TreeNode root) {
if(root == null) return "";
StringBuilder sb = new StringBuilder();
preDfs(sb,root);
return sb.toString();
}
//使用前序遍历进行序列化树
void preDfs(StringBuilder sb,TreeNode root){
if(root == null){
sb.append("#,");
return;
}
//根左右
sb.append(root.val).append(",");
preDfs(sb,root.left);
preDfs(sb,root.right);
}
TreeNode Deserialize(String str) {
if(str == null || str.length() < 1) return null;
return buildTree(str);
}
int index = 0; //记录遍历到字符串下标
TreeNode buildTree(String str){
if(str.charAt(index) == '#'){
index += 2; // 跳过#和,
return null;
}
//考虑二叉树中的值为负数的结点
boolean is_minus = false;
if(str.charAt(index) == '-'){
is_minus = true;
index++; // 跳过减号
}
int t = 0;
//考虑二叉树的值为非个数的情况
while(str.charAt(index) != ','){
t = t * 10 + str.charAt(index) - '0';
index++; //算好一个数的每位数
}
index++; //跳过逗号
if(is_minus) t = -t; //到这里已经正确获得结点上面的值了
TreeNode root = new TreeNode(t);
root.left = buildTree(str);
root.right = buildTree(str);
return root;
}
}
JZ62 二叉搜索树的第k个结点
题目描述
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
解题思路
(1)最简单的思路就是用一个辅助数组给存起来,然后再排序获得第k小的结点。但是这样显然浪费了二叉搜索树的性质,所以这样写不是我们的考点。
(2)这里我们要注意到,题目给定的是一颗二叉搜索树,请找出其中的第k小的结点。而一颗二叉搜索树的中序遍历其实就是一个递增的序列。而我们的所要做的就是声明一个全局变量,在遍历到我们的k的时候停止就行了。实现代码如下:
实现代码
public class Solution {
int count = 0;
TreeNode root;
TreeNode KthNode(TreeNode pRoot, int k)
{
if(pRoot == null || k < 0) return null;
dfs(pRoot,k);
return root;
}
//进行三个条件判断才进行查找,这样就可以避免无限查找下去
void dfs(TreeNode pRoot,int k){
if(count < k && pRoot.left != null) dfs(pRoot.left,k);
if(++count == k) root = pRoot;
if(count < k && pRoot.right != null) dfs(pRoot.right,k);
}
}
JZ63 数据流中的中位数
题目描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
解题思路
维护一个大顶堆和一个小顶堆,保证小顶堆的最大值是小于大顶堆的最小值。然后如果为奇数的话,那么小顶堆可以比大顶堆的长度大1,如果是偶数则一样长。然后我们在长度的的相等的时候,也就是偶数的时候只需要将两个堆顶求平均数就行,如果小顶堆的长度大于大顶堆的话,那说明是奇数的个数,只需要返回小顶堆的堆顶就行。
其实同理,这里维护两个单调栈也行,时间复杂度应该更小,毕竟堆还要自己维护排序。
实现代码
import java.util.*;
public class Solution {
PriorityQueue<Integer> highQueue = new PriorityQueue<>(); // 小顶堆保存大数据部分
PriorityQueue<Integer> lowQueue = new PriorityQueue<>((Comparator<Integer>) (o1, o2) -> o2 - o1);// 大顶堆保存小数据部分
public void Insert(Integer num) { // 保持两个堆平衡即size之差为1(默认lowQueue比highQueue >= 1)
lowQueue.offer(num);// 数据线往小堆扔
highQueue.offer(lowQueue.poll());// 保证存到大堆的永远是小队中的最大值(即保证数据被分为大、小两部分)
if (highQueue.size() > lowQueue.size()) {// 若此时大堆容量大于小堆则移出大堆中最小数据至小堆
lowQueue.offer(highQueue.poll());
}
}
public Double GetMedian() {
return lowQueue.size() == highQueue.size() ?
((lowQueue.peek() + highQueue.peek()) * 0.5) : Double.valueOf(lowQueue.peek());
}
}
JZ64 滑动窗口的最大值
题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
窗口大于数组长度的时候,返回空
解题思路&实现代码
(1)最开始想到的,但是时间复杂度过高,不推荐。维护一个size大小的大根堆,然后每次删除left指针的元素和添加right指针的元素,并添加堆顶的元素。缺点就是堆删除的时间复杂度很高,可能这样操作还不如直接暴力来的快,所以这算是错误示范。
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size)
{
ArrayList<Integer> list = new ArrayList<>();
//考虑三种特殊情况
if(num.length < 1 || num.length < size || size < 1) return list;
//设置为大顶堆
PriorityQueue<Integer> pq = new PriorityQueue<>((v1,v2)->v2 - v1);
int left = 0,right = size;
int len = num.length;
//先初始化将size大小的
for(int i = 0; i < size; i++){
pq.offer(num[i]);
}
list.add(pq.peek()); //先添加第一个最大的
while(right < len && left < right){
pq.remove(num[left]);
pq.offer(num[right]);
list.add(pq.peek());
left++;
right++;
}
return list;
}
}
(2)待补充...
JZ65 矩阵中的路径
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子
解题思路
对矩阵里面的每一个点进行深搜,查找到匹配路径返回true,遇到不符合情况的返回false。然后重点就是要标记我们的走过的路,在走过的时候进行标记走过。在搜索过后要取消我们之前标记过的路径。
实现代码
//定义我们的标记数组
private static int[][] visted;
public static boolean hasPath(char[] matrix, int rows, int cols, char[] str)
{
//初始化我们的标记数组
visted = new int[rows][cols];
for(int i = 0; i <= rows-1; i++){
for(int j = 0; j <= cols-1; j++){
//对每一个结点进行搜索
if(isPath(matrix, rows, cols,i,j,0,str))
return true ;
}
}
return false;
}
//搜索+回溯 考虑好边界处理就行
//注意对我们搜过的路径进行回溯取消标记
public static boolean isPath(char[] matrix, int rows, int cols, int x, int y,int k,char[] str){
if(k == str.length){
return true;
}
if(x<0 || y<0 || x>=rows ||y>=cols || visted[x][y]==1){
return false;
}
if(str[k] != matrix[x* cols+y]){
return false;
}
visted[x][y] = 1;
if(isPath(matrix,rows,cols,x+1,y,k+1,str)) return true;
if(isPath(matrix,rows,cols,x,y+1,k+1,str)) return true;
if(isPath(matrix,rows,cols,x-1,y,k+1,str)) return true;
if(isPath(matrix,rows,cols,x,y-1,k+1,str)) return true;
visted[x][y] = 0;
return false;
}
JZ66 机器人的运动范围
题目描述
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解题思路
我们可以从(0,0)开始,进行深搜,对于走过的进行标记,同时对于每一个要走的进行判断数位相加有没有大于k。最后返回所有的数就是我们要的路径之和。
实现代码
public class Solution {
//定义方向数组
private int[] x = {0,1,-1,0};
private int[] y = {1,0,0,-1};
public int movingCount(int k,int m, int n) {
if(k < 1) return 0; //特判,k小于1的时候,是不能移动的。
boolean[][] flag = new boolean[m][n];
return dfs(flag,0,0,k);
}
private int dfs(boolean[][] flag,int m,int n,int k){
//标记走过的为true
flag[m][n] = true;
int sum = 1;
for(int i = 0; i < x.length; i++){
int dx = m+x[i];
int dy = n+y[i];
//对边界进行判断
if(dx < flag.length && dx >= 0 && dy < flag[0].length && dy >=0){
//对数位的之和与k进行判断和判断是否走过该格子
if(numOfSum(dx)+numOfSum(dy) <= k && flag[dx][dy] != true){
sum += dfs(flag,dx,dy,k);
}
}
}
return sum;
}
//由于最大的数不超过100,所以直接对这几位数位进行相加
private int numOfSum(int a){
return a/100+a/10+a%10;
}
}
JZ67 剪绳子
题目描述
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1,m<=n),每段绳子的长度记为k[1],...,k[m]。请问k[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
解题思路
动态规划。我们可以把一段绳子的长度看成若干个子绳子的长度,然后求一段绳子的最大乘积,就是求每段子绳子的长度的最大的积的积。
实现代码
public class Solution {
public int cutRope(int target) {
if(target < 2) return 0;
if(target == 2) return 1;
if(target == 3) return 2;
int[] dp = new int[target + 1];
//用来供后面使用的初始化前缀
for(int i = 1; i <= 4; i++){
dp[i] = i;
}
// 举例,这里5 可以看成 2的最大乘积*3的最大乘积或者1的最大乘积*4的最大乘积....
// 同理,后面同样的也可以这样看成一个个分割的子问题,所以就可以用动态规划来写
for(int i = 5; i <= target; i++){
for(int j = 1; j < i; j++){
dp[i] = Math.max(dp[i],dp[i - j] * j);
}
}
return dp[target];
}
}