题目:
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。
给定 target = 3,返回 false。
数据范围:矩阵的长宽满足 0≤n,m≤500 , 矩阵中的值满足 0≤val≤10^9
进阶:空间复杂度 O(1)O(1) ,时间复杂度 O(n+m)O(n+m)
解析:
题解1:可以利用for循环逐行逐列遍历,找到目标元素。
题解2:题目中的数组具有一定的规律,可以发现一个元素大于其上方的元素,小于其邮编的元素,并且每一行最右边的元素是最大的,利用分治思维来解题。
可以先将目标元素和第一行最右边元素比较,若小于目标元素,则元素下移,若大于目标元素,则元素左移。
代码:
注意边界:j>=0,自己在写的时候疏忽了等于,然后若目标值等于array[0][0]的时候,就检测不到了。
public class Solution {
public boolean Find(int target, int [][] array) {
if (array == null) {
return false;
}
int i = 0;
int j = array[0].length - 1;
while (i < array.length && j >= 0) {
if (target < array[i][j]) {
j--;
} else if (target > array[i][j]) {
i++;
} else {
return true;
}
}
return false;
}
}
链接:https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?
相同类型思路题目:
题目:
请实现无重复数字的升序数组的二分查找
给定一个 元素升序的、无重复数字的整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标(下标从 0 开始),否则返回 -1
数据范围:0≤len(nums)≤2×10^5
数组中任意值满足∣val∣≤10^9
进阶:时间复杂度O(logn) ,空间复杂度O(1)
代码:
import java.util.*;
public class Solution {
public int search (int[] nums, int target) {
int i = 0;
int j = nums.length - 1;
while (i <= j) {
int m = (i + j) / 2;
if (nums[m] < target) {
i = m+1;
} else if (nums[m] > target) {
j = m-1;
} else {
return m;
}
}
return -1;
}
}
题目:
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。
1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于
2.假设 nums[-1] = nums[n] = -\infty−∞
3.对于所有有效的 i 都有 nums[i] != nums[i + 1]
4.你可以使用O(logN)的时间复杂度实现此问题吗?
代码:
import java.util.*;
public class Solution {
public int findPeakElement (int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] > nums[mid + 1]) {
right = mid;
} else {
left = mid + 1;
}
}
return right;
}
}
疑问:输入这个[2,4,1,2,7,8,8,4]测试是错的,但是系统说是对的,测出来输出是6,但是8不是峰值,因为其和左边是相同数值。
题目:
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
数据范围:1≤n≤10000,数组中任意元素的值:100000≤val≤10000
要求:空间复杂度:O(1)O(1) ,时间复杂度:O(logn)O(logn)
方法一:遍历。按照要求,要么是一个非递减排序的数组(最小值在最开始),要么是一个旋转(最小值在中间某个地方) ,而且,旋转之后有个特征,就是在遍历的时候,原始数组是非递减的,旋转之后,就有可能出现递减,引起递减的数字,就 是最小值。
import java.util.*;
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
if (array == null || array.length == 0) {
return 0;
}
for (int i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
return array[i + 1];
}
}
return array[0];
}
}
方法二:利用二分查找
代码:
import java.util.*;
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
// 不要忘记特殊情况判断
if (array == null || array.length == 0) {
return 0;
}
int i = 0;
int j = array.length - 1;
while (i < j) {
int m = ( i + j ) / 2;
// m在左排序数组中,旋转点在 [m+1, j] 中
if (array[m] > array[j]) {
i = m + 1;
}
// m 在右排序数组中,旋转点在 [i, m]中
else if (array[m] < array[j]) {
j = m;
}
// 缩小范围继续判断
else {
j--;
}
}
return array[j];
}
}
注意:在array[m] < array[j]) 的时候,旋转点在[i,m]中,注意边界,此处旋转点数值是可能为array[m]的。
总结:注意条件语句中条件的编写,是否可取等号,以及在某条件下赋值时要具体问题具体分析,特别注意边界问题。
题目:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解析:
本题的难点在于保证相对位置不变。
利用插入排序的思想,从前向后遍历,遇到偶数继续向后遍历,遇到奇数,则插入到第一个偶数的前面(将遍历到的奇数的前面的所有偶数整体后移即可)不断重复该操作,即可。
代码:
import java.util.*;
public class Solution {
public void reOrderArray(int [] array) {
// 思路:找到一个奇数,然后将其前面的偶数后移,将该奇数挪到前面
// 记录下一个应该是奇数的位置,从第一个开始。
int k = 0;
// 从左向右,每次遇到的,都是最前面的奇数,一定将来要被放在k下标处
for (int i = 0; i < array.length; i++) {
// 利用二进制的位运算判断是否是奇数
if ((array[i] & 1) == 1) {
int temp = array[i];
int j = i;
// 将奇数之前的偶数整体后移
while (j > k) {
array[j] = array[j - 1];
j--;
}
//将奇数保存在它将来改在的位置,因为我们是从左往右放的,没有跨越奇数,所以一定是相对位置不变的
array[k++] = temp;
}
}
}
}
发现:
利用二进制的位运算判断是否是奇数,这比取模运算效率更高。
按位与运算: 1&1=1 ,1&0=0,0&0=0;
奇数的二进制低位一定是1,且1的二进制只有低位是1,其他位都是0。偶数的二进制低位一定是0。所以1和奇数 & 运算一定等于1。1和偶数 & 运算一定等于0。
题目:
给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如输入一个长度为9的数组[1,2,3,2,2,2,5,4,2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。
数据范围:n≤50000,数组中元素的值100000≤val≤10000
要求:空间复杂度:O(1),时间复杂度 O(n)
解析与代码:
思路一:定义map,使用<数字,次数>的映射关系,最后统计每个字符出现的次数。
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if (array == null) {
return 0;
}
HashMap<Integer, Integer> mp = new HashMap<Integer, Integer>();
for (int i = 0; i < array.length; i++) {
if (!mp.containsKey(array[i])) {
mp.put(array[i], 1);
} else {
mp.put(array[i], mp.get(array[i]) + 1);
}
if (mp.get(array[i]) > array.length / 2) {
return array[i];
}
}
return 0;
}
}
思路二:排序之后,出现次数最多的数字,一定在中间位置。然后检测中间出现的数字出现的次数是否符合要求 。
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if (array == null) {
return 0;
}
Arrays.sort(array);
int mid = array.length / 2;
int target = array[mid];
int count = 0;
for (int i = 0; i < array.length; i++) {
if (target == array[i]) {
count++;
}
}
if (count > mid) {
return target;
}
return 0;
}
}
思路三:目标数据超过数组长度的一半,那么对数组,我们同时去掉两个不同的数字,到最后剩下的一个数就是 该数字。如果剩下两个,那么这两个也是一样的,就是结果),在其基础上把最后剩下的一个数字或者两个回到原来数组中, 将数组遍历一遍统计一下数字出现次数进行最终判断。
import java.util.*;
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if (array == null) {
return 0;
}
int target = array[0];
int times = 1;
// 下面这波操作很微妙
for (int i = 1; i < array.length; i++) {
if (times == 0) {
target = array[i];
times = 1;
} else if (array[i] == target) {
times++;
} else {
times--;
}
}
times = 0;
for (int i = 0; i < array.length; i++) {
if (target == array[i]) {
times++;
}
}
return times > array.length / 2 ? target : 0;
}
}
题目:
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
解析与代码:
思路一:利用空间换时间
import java.util.*;
public class Solution {
public String replaceSpace(StringBuffer str) {
StringBuilder sb = new StringBuilder();
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(c == ' '){
sb.append("%20");
}else{
sb.append(c);
}
}
return sb.toString();
}
}
思路二:替换问题,但是生成的字符串整体变长了. 因替换内容比被替换内容长,所以,一定涉及到字符串中字符的移动问题,移动方向一定是向后移动,所以现在的问题无非是移动多少的问题。
因为是 ’ ’ -> “%20”,是1换3,所以可以先统计原字符串中空格的个数(设为n),然后可以计算出新字符串的长度,即:new_length = old_length + 2*n 。最后,定义新老索引(或者指针),各自指向新老空间的结尾,然后进行old->new的移动。如果是空格,就连续放入“%20”,其他平移即可。
import java.util.*;
public class Solution {
public String replaceSpace(StringBuffer str) {
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == ' ') {
count++;
}
}
int new_l = str.length() + 2 * count;
int new_end = new_l-1;
int old_end = str.length()-1;
str.setLength(new_l);
while(new_end>=0&&old_end>=0){
if(str.charAt(old_end)==' '){
str.setCharAt(new_end--,'0');
str.setCharAt(new_end--,'2');
str.setCharAt(new_end--,'%');
--old_end;
}else{
str.setCharAt(new_end--,str.charAt(old_end));
--old_end;
}
}
return str.toString();
}
}
思路一是空间换时间,思路二则空间占用少很多。
题目:
输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
如输入{1,2,3}的链表如下图:
返回一个数组为[3,2,1]
0 <= 链表长度 <= 10000
解析与代码:
思路一:
第一个想到的就是栈。
import java.util.*;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
Stack<Integer> st = new Stack<>();
while (listNode != null) {
st.push(listNode.val);
listNode = listNode.next;
}
ArrayList<Integer> arr = new ArrayList<>();
while (!st.isEmpty()) {
arr.add(st.pop());
}
return arr;
}
}
判断栈是否为空用的是isEmpty(),自己最开始潜意识写成了==null。
思路二:将链表中的数保存到数组中,之后将数组逆置。
import java.util.*;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
while (listNode != null) {
list.add(listNode.val);
listNode = listNode.next;
}
int i = 0;
int j = list.size() - 1;
while (i < j) {
Integer temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
i++;
j--;
}
return list;
}
}
思路三:
递归
import java.util.*;
public class Solution {
ArrayList<Integer> list = new ArrayList<>();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if (listNode != null) {
printListFromTailToHead(listNode.next);
list.add(listNode.val);
}
return list;
}
}
思路四:
ArrayList 中有个方法是 add(index,value)
,可以指定 index 位置插入 value 值
所以我们在遍历 listNode 的同时将每个遇到的值插入到 list 的 0 位置,最后输出 listNode 即可得到逆序链表
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
ListNode temp = listNode;
while (temp != null) {
list.add(0, temp.val);
temp = temp.next;
}
return list;
}
}
题目:
给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点。
例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出如下图所示。
提示:
1.vin.length == pre.length
2.pre 和 vin 均无重复元素
3.vin出现的元素均出现在 pre里
4.只需要返回根结点,系统会自动输出整颗树做答案对比
数据范围:n≤2000,节点的值 −10000≤val≤10000
要求:空间复杂度 O(n),时间复杂度 O(n)
解析和代码:
思路一:
利用递归。
import java.util.*;
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre, int [] vin) {
int m = pre.length;
int n = vin.length;
if (m == 0 || n == 0) {
return null;
}
TreeNode root = new TreeNode(pre[0]);
for (int i = 0; i < n; i++) {
if (pre[0] == vin[i]) {
int[] left_pre = Arrays.copyOfRange(pre, 1, i + 1);
int[] left_vin = Arrays.copyOfRange(vin, 0, i);
int[] right_pre = Arrays.copyOfRange(pre, i + 1, m);
int[] right_vin = Arrays.copyOfRange(vin, i + 1, n);
root.left = reConstructBinaryTree(left_pre, left_vin);
root.right = reConstructBinaryTree(right_pre, right_vin);
break;
}
}
return root;
}
}
思路二:非递归,略。
题目:
大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。 0 1 1 2 3 5 8 13 21数据范围:1≤n≤40
要求:空间复杂度O(1),时间复杂O(n) ,本题也有时间复杂度 O(logn)的解法。
解析与代码:
思路一:递归
public class Solution {
public int Fibonacci(int n) {
if(n<=1){
return n;
}
else{
return Fibonacci(n-1)+Fibonacci(n-2);
}
}
}
时间复杂度:O(2^n),不符合题目要求。
思路二:迭代(推荐,刚刚好时间和空间复杂度都符合要求)
public class Solution {
public int Fibonacci(int n) {
if (n <= 1) {
return n;
}
int a = 0;
int b = 1;
int su = 0;
for (int i = 2; i <= n; i++) {
su = a+b;
a = b;
b = su;
}
return su;
}
}
https://www.nowcoder.com/questionTerminal/db7ec6d0b8254237bf023712fa433bb5?orderByHotValue=1&page=4
小招喵出生自大家族,这一天,小招喵拿到了生涯中的第一桶金,他就想给族人买鞋子,但是,为了后续生计,小招喵最多花费m 元。小招喵来到了商场,在柜台上初步挑选了 n 双鞋子,编号为1 ~ n,其中,第i 双鞋的售价为ai,实用价值可以量化为bi ,擅长心算的小招喵马上就意识到,这是家黑店,售价与实用价值并不成比例。于是,小招喵决定发挥自己的聪明才智,他在店家不注意的时候,偷偷更换了若干双鞋子的价格标签(也可能不做任何更换),那么,这一顿操作之后,在花费不超过限额的前提下,小招喵最多可以购买到实用价值总和为多少的鞋子?(假设小招喵的操作不会被店家发现,而且,店家严格按照鞋子的价格标签售卖)只需要实用价值总和最大,而不要求花费必须最少。
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 返回能购买的最大的实用价值
* @param m int整型 牛牛的花费限额
* @param sellPrice int整型一维数组 每双鞋的售价
* @param realValue int整型一维数组 每双鞋的实用价值
* @return int整型
*/
public int maxRealValue (int m, int[] sellPrice, int[] realValue) {
// write code here
Arrays.sort(sellPrice);
Arrays.sort(realValue);
int sum_sell = 0, sum_real = 0;
for(int i = 0; i < sellPrice.length; i++){
sum_sell += sellPrice[i];
if( sum_sell > m){
break;
}
sum_real += realValue[sellPrice.length - i - 1];
}
return sum_real;
}
}
题目:
有两种蛋糕,榴莲蛋糕和冰淇淋蛋糕,其中榴莲蛋糕被分成了a块,冰淇淋蛋糕被分成了b块。总共有个n盘子,每种蛋糕能够整除均分到各个盘子里,要求一个盘子里面不能含有两种蛋糕,每个盘子至少含有2块蛋糕,所有的蛋糕必须放到盘子里面去。求一个盘子里最少有多少块蛋糕。不能做到的话返回0。
解析:
参考牛客网上的解析,此处仅用于自己记录。
代码:
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
* @param n int整型 n个盘子
* @param a int整型 榴莲蛋糕a块
* @param b int整型 冰淇淋蛋糕b块
* @return int整型
*/
public int minCake (int n, int a, int b) {
// write code here
if(a + b == n || Math.min(a, b) < 2){
return 0;
}else if(n == 2){
return Math.min(a, b);
}
if(a < b){
int temp = a;
a = b;
b = temp;
}
// 把a均分放入i个盘子
int ans = a;
for(int i = 1; i <= n - 1; i++){
if(a % i != 0){
continue;
}else{
int j = n - i;
if(b % j != 0){
continue;
}else{
ans = Math.max(2, Math.min(ans, Math.min(a / i, b / j)));
}
}
}
return ans;
}
}
题目:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
数据范围:1≤n≤40
要求:时间复杂度:O(n),空间复杂度:O(1)
解析与代码:
思路一:
递归。如果我从第n个台阶进行下台阶,下一步有2中可能,一种走到第n-1个台阶,一种是走到第n-2个台阶。所以f[n] = f[n-1] + f[n-2]. 那么初始条件了,f[0] = f[1] = 1。
但是时间复杂度不符合要求。利用递归时间复杂度达到了O(2^n) 。
public class Solution {
public int jumpFloor(int target) {
if (target <= 1) {
return 1;
}
return jumpFloor(target - 1) + jumpFloor(target - 2);
}
}
思路二:
将其看成 斐波拉契数列 问题。
public class Solution {
public int jumpFloor(int target) {
int num0 = 1;
int num1 = 1;
int temp;
while(target>1){
temp = num1+num0;
num0=num1;
num1=temp;
target--;
}
return num1;
}
}
public class Solution {
public int jumpFloor(int target) {
int num0 = 1;
int num1 = 1;
int temp;
for (int i = 2; i <= target; i++) {
temp = num1 + num0;
num0 = num1;
num1 = temp;
}
return num1;
}
}
思路三:
动态规划。
状态定义:f(i): 跳到i台阶的总跳法
状态递推:f(i) = f(i-1)+f(i-2)
初始状态: f(0) = 1(0台阶,就是起点,到达0台阶的方法有一种,就是不跳[这里可能有点奇怪,但是想想,如果方 法次数为0,就说明不可能开始…]), f(1) = 1;
public class Solution {
public int jumpFloor(int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= target; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[target];
}
}
题目:
我们可以用 21 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 21 的小矩形无重叠地覆盖一个 2n 的大矩形,从同一个方向看总共有多少种不同的方法?
数据范围:0≤n≤38
进阶:空间复杂度O(1) ,时间复杂度 O(n)
注意:约定 n == 0 时,输出 0
比如n=3时,23的矩形块有3种不同的覆盖方法(从同一个方向看):
解析与代码:
动态规划。
每次放置的时候,无非两种放法,横着放或竖着放。其中,横着放一个之后,下一个的放法也就确定了,故虽然放置了两个矩形,但属于同一种放法。其中,竖着放一个之后,本轮放置也就完成了,也属于一种方法。所以,矩形被放满的时候,它无非就是从上面两种放置方法放置来的。
状态定义:f(n) : 用n个21的小矩形无重叠地覆盖一个2n的大矩形所用的总方法数
状态递推:f(n) = f(n-1)【最后一个竖着放】 + f(n-2)【最后两个横着放】
初始化:f(1) = 1,f(2) = 2,f(0)=1,注意f(0)我们这个可以不考虑,如果考虑值设为1。
import java.util.*;
public class Solution {
public int rectCover(int target) {
if (target <= 2) {
return target;
}
int[] dp = new int[target + 1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= target ; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[target];
}
}
其实也就是斐波拉契问题。只是我好像不会用动态规划的思维去考虑问题。
题目:
输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。
数据范围:−2^31 <=n<=2 ^31 −1
即范围为:−2147483648<=n<=2147483647
解析与代码:
思路一:
位运算和移位运算。
我们可以检查该数字的二进制每一位是否为1,可以考虑移位运算,每次移动一位,以此遍历二进制的每一位。
数字1与数字相位与运算,其实只是最后一位为1就是1,最后一位为0就是0,这样我们只需要将数字1移位运算,就可以遍历二进制的每一位,再去做位与运算,结果为1的就是二进制中为1的。
import java.util.*;
public class Solution {
public int NumberOf1(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
if ((n & (1 << i)) != 0) {
count++;
}
}
return count;
}
}
思路二:
思路一的扩展。
有一个性质:n&(n−1),会将n的二进制中最低位由1变成0。
我们可以不断让当前的 n与 n−1做位与运算,直到 n的二进制全部变为 0 停止。因为每次运算会使得 nnn 的最低位的 1 被翻转成0,因此运算次数就等于 n 的二进制位中 1 的个数,由此统计1的个数。
import java.util.*;
public class Solution {
public int NumberOf1(int n) {
int count = 0;
while (n != 0) {
n &= (n - 1);
count++;
}
return count;
}
}
题目:
输入一个链表,输出该链表中倒数第k个结点。
解析与代码:
题目中的链表是单链表,也就不能从后往前进行。可以定义两个指针,一个指针先走k步,再让另一个指针跟在后面,使用“前后指针”的方式,当前面的指针到达结尾, 后面的指针,也就是倒数第k个节点。
import java.util.*;
public class Solution {
public ListNode FindKthToTail(ListNode head, int k) {
if (head == null || k < 0) {
return null;
}
ListNode f = head;
ListNode r = head;
while (k > 0 && f != null) {
f = f.next;
k--;
}
while (f != null) {
f = f.next;
r = r.next;
}
return k > 0 ? null : r;
}
}
题目:
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围:0≤n≤1000
要求:空间复杂度O(1) ,时间复杂度O(n) 。
解析:
参考讲解:https://www.bilibili.com/video/BV13v411h7HR?vd_source=e5a575003d205f54dc71d7815361a220
思路一:
迭代求解。
import java.util.Stack;
public class Solution {
public ListNode ReverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
思路二:
利用递归。
import java.util.Stack;
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode p = ReverseList(head.next);
head.next.next = head;
head.next = null;
return p;
}
}
思路三:
利用栈先进后出的性质。
import java.util.Stack;
public class Solution {
public ListNode ReverseList(ListNode head) {
Stack<ListNode> stack = new Stack<>();
while(head !=null){
stack.push(head);
head=head.next;
}
if(stack.isEmpty()){
return null;
}
ListNode node = stack.pop();
ListNode dummy = node;
while(!stack.isEmpty()){
ListNode temp = stack.pop();
node.next = temp;
node=node.next;
}
// 最后一个节点是反转前的头结点,需将其next等于null,否则会构成环。
node.next=null;
return dummy;
}
}
题目:
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
数据范围:0≤n≤1000,−1000≤节点值≤1000
要求:空间复杂度 O(1),时间复杂度 O(n)
解析:
思路一:
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null && list2 == null){
return null;
}
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
// 定义合并后链表的首尾节点
ListNode head_node = null;
ListNode last_node = null;
// 获取list1和list2中数据值较小的节点,并设置为合并后链表的首尾节点
if(list1.val > list2.val){
head_node = list2;
last_node = list2;
list2= list2.next;
}else{
head_node = list1;
last_node = list1;
list1 = list1.next;
}
// 依次获取两个链表中的数据值较小的节点,并将其添加到合并后的链表的表尾
while(list1 != null && list2 != null){
if(list1.val > list2.val){
last_node.next = list2;
last_node = list2;
list2= list2.next;
}else{
last_node.next = list1;
last_node = list1;
list1 = list1.next;
}
}
// 循环结束后,如果某链表的首节点不为null,则将该链表此刻的首节点以及其后的所有节点添加到合并后的链表表尾。
if(list1 != null){
last_node.next = list1;
}else{
last_node.next = list2;
}
return head_node;
}
}
思路二:
递归。
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null||list2==null){
return list1!=null?list1:list2;
}
if(list1.val<=list2.val){
list1.next=Merge(list1.next,list2);
return list1;
}else{
list2.next=Merge(list1,list2.next);
return list2;
}
}
}
题目:
输入两棵二叉树A,B,判断B是不是A的子结构。(我们约定空树不是任意一个树的子结构)
假如给定A为{8,8,7,9,2,#,#,#,#,4,7},B为{8,9,2},2个树的结构如下,可以看出B是A的子结构。
数据范围:
0 <= A的节点个数 <= 10000
0 <= B的节点个数 <= 10000
**解析: **
import java.util.*;
public class Solution {
public boolean HasSubtree(TreeNode root1, TreeNode root2) {
//空树
if (root2 == null || root1 == null)
return false;
boolean result = false;
if (root1.val == root2.val) {
result = recursion(root1, root2);
}
if (result != true) {
result = HasSubtree(root1.left, root2);
}
if (result != true) {
result = HasSubtree(root1.right, root2);
}
return result;
}
private boolean recursion(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 != null )
return false;
// 说明比较完了
if (root2 == null)
return true;
//比较节点值
if (root1.val != root2.val)
return false;
//递归比较子树
return recursion(root1.left, root2.left) && recursion(root1.right, root2.right);
}
}
题目:
操作给定的二叉树,将其变换为源二叉树的镜像。
数据范围:二叉树的节点数 0≤n≤1000 , 二叉树每个节点的值 0≤val≤1000
要求: 空间复杂度 O(n) 。本题也有原地操作,即空间复杂度 O(1) 的解法,时间复杂度 O(n)
解析:
思路一:
利用递归。二叉树镜像本质是自顶向下(or自底向上)进行左右子树交换的过程。
import java.util.*;
public class Solution {
public TreeNode Mirror (TreeNode pRoot) {
if (pRoot == null) {
return null;
}
TreeNode temp = pRoot.right;
pRoot.right = pRoot.left;
pRoot.left = temp;
Mirror(pRoot.left);
Mirror(pRoot.right);
return pRoot;
}
}
思路二:
利用栈(没太懂)
import java.util.*;
public class Solution {
public TreeNode Mirror (TreeNode pRoot) {
if(pRoot==null){
return null;
}
Stack<TreeNode> s = new Stack<TreeNode>();
s.push(pRoot);
while(!s.isEmpty()){
TreeNode node = s.pop();
if(node.left!=null){
s.push(node.left);
}
if(node.right!=null){
s.push(node.right);
}
TreeNode temp = node.left;
node.left=node.right;
node.right=temp;
}
return pRoot;
}
}
题目:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5。
数据范围:链表长度满足0≤n≤1000 ,链表中的值满足 1≤val≤1000
进阶:空间复杂度 O(n) ,时间复杂度 O(n)
解析:
题目中已经说到这是一个已经排序好的序列,所以相同的数字相邻。
方法一:
直接判断相邻节点的值是否相等,如果相等则判断后面是否有多个相同元素,然后删除所有元素。
import java.util.*;
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null) {
return null;
}
// 有时候可能会全部相同,在链表前加一个表头会更简单。
ListNode pre = new ListNode(0);
pre.next = pHead;
ListNode cur = pre;
while (cur.next != null && cur.next.next != null) {
// 遇到相邻的两个值相同
if (cur.next.val == cur.next.next.val) {
int temp = cur.next.val;
// 将所有相同的都跳过
while (cur.next != null && cur.next.val == temp) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
// 返回的时候去掉表头
return pre.next;
}
}
import java.util.*;
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null) {
return pHead; //走到这里结果一共有三种,注意:prev永远指向的是前驱有效起始节点: //1. last.next != null 并且 (prev, last] 限定了一段重复范围,此时进行去重 //2. last.next == null && (prev, last] 限定了一段重复范围,此时进行去重,最后相当于prev- >next = nullptr //3. last.next == null && prev.next == last,这说明,从本次循环开始,大家都不相同,就不需 要进行去重,这个是特殊情况 if(prev.next != last){ 比特就业课
}
ListNode head = new ListNode(0);
head.next = pHead;
ListNode prev = head;
ListNode last = prev.next;
while (last !=
null) { //last永远在前面
//先找到重复的开始
while (last.next != null && last.val != last.next.val) {
prev = prev.next;
last = last.next;
}
//在找到重复的范围
while (last.next != null && last.val == last.next.val) {
last = last.next;
}
//走到这里结果一共有三种,注意:prev永远指向的是前驱有效起始节点:
//1. last.next != null 并且 (prev, last] 限定了一段重复范围,此时进行去重
//2. last.next == null && (prev, last] 限定了一段重复范围,此时进行去重,最后相当于prev- >next = nullptr
//3. last.next == null && prev.next == last,这说明,从本次循环开始,大家都不相同,就不需 要进行去重,这个是特殊情况
if (prev.next != last) {
//说明是一段范围,可以去重
prev.next = last.next;
}
last = last.next; //走这一步,就是为了保证恢复的和最开始一致
}
return head.next;
}
}
方法二:
利用哈希表,首先统计节点值出现的次数。这种方法对于无序的链表也是适用的。
step 1:遍历一次链表用哈希表记录每个节点值出现的次数。
step 2:在链表前加一个节点值为0的表头,方便可能的话删除表头元素。
step 3:再次遍历该链表,对于每个节点值检查哈希表中的计数,只留下计数为1的,其他情况都删除。
step 4:返回时去掉增加的表头。
import java.util.*;
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
//空链表
if (pHead == null) {
return null;
}
HashMap<Integer, Integer> mp = new HashMap<>();
ListNode cur = pHead;
//遍历链表统计每个节点值出现的次数
while (cur != null) {
if (mp.containsKey(cur.val)) {
mp.put(cur.val, (mp.get(cur.val) + 1));
} else {
mp.put(cur.val, 1);
}
cur = cur.next;
}
//在链表前加一个表头
ListNode res = new ListNode(0);
res.next = pHead;
cur = res;
//再次遍历链表
while (cur.next != null) {
//如果节点值计数不为1,删除节点
if (mp.get(cur.next.val) != 1) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
// 去掉加的表头
return res.next;
}
}
题目:
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。
此栈包含的方法有:
push(value):将value压入栈中
pop():弹出栈顶元素
top():获取栈顶元素
min():获取栈中最小元素
数据范围:操作数量满足0≤n≤300 ,输入的元素满足∣val∣≤10000
进阶:栈的各个操作的时间复杂度是 O(1),空间复杂度是 O(n)
解析:
很容易想到,在栈内部保存min变量,每次更新的时候,都对min变量进行更新。
但是,面试官很容易就会问到:如果想拿出第二小,第三小的值怎么拿?。用上面的办法就不行了。
为了满足通用,我们使用一个辅助栈,内部保存元素的个数和数据栈完全一样,不过,辅助栈内部永远保存本次入栈的数为 所有数据的最小值(注意:辅助栈内部元素可能会出现“必要性”重复)
import java.util.Stack;
public class Solution {
Stack<Integer> s1 = new Stack<Integer>();
Stack<Integer> s2 = new Stack<Integer>();
public void push(int node) {
s1.push(node);
if(s2.isEmpty()||s2.peek()>node){
s2.push(node);
}
else{
s2.push(s2.peek());
}
}
public void pop() {
s1.pop();
s2.pop();
}
public int top() {
return s1.peek();
}
public int min() {
return s2.peek();
}
}
题目:
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
解析:
step 1:准备一个辅助栈,两个下标分别访问两个序列。
step 2:辅助栈为空或者栈顶不等于出栈数组当前元素,就持续将入栈数组加入栈中。
step 3:栈顶等于出栈数组当前元素就出栈。
step 4:当入栈数组访问完,出栈数组无法依次弹出,就是不匹配的,否则两个序列都访问完就是匹配的。
以遍历出栈为基础进行for循环:
import java.util.*;
import java.util.ArrayList;
public class Solution {
public boolean IsPopOrder(int [] pushA, int [] popA) {
//辅助栈
Stack<Integer> st = new Stack<>();
int j = 0;
//遍历出栈的数组
for(int i = 0;i < popA.length; i++){
//入栈:栈为空或者栈顶不等于出栈数组
while(j< pushA.length &&(st.isEmpty() || st.peek() !=popA[i])){
st.push(pushA[j]);
j++;
}
//栈顶等于出栈数组
if(st.peek() == popA[i] ){
st.pop();
}else{ //不匹配序列
return false;
}
}
return true;
}
}
以遍历入栈为基础进行for循环:
import java.util.*;
import java.util.ArrayList;
public class Solution {
public boolean IsPopOrder(int [] pushA, int [] popA) {
if (pushA == null || popA == null || pushA.length == 0 || popA.length == 0 ||
pushA.length != popA.length) {
return false;
}
// 辅助栈
Stack<Integer> st = new Stack<>();
int j = 0;
// 遍历入栈
for (int i = 0; i < pushA.length; i++) {
st.push(pushA[i]);
while (!st.empty() && st.peek() == popA[j] ) {
st.pop();
j++;
}
}
// 判断栈是否为空
return st.empty();
}
}
题目:
不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回。
解析:
思路一:层次遍历
step 1:首先判断二叉树是否为空,空树没有遍历结果。
step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面。
step 3:每次遍历队首节点,如果它们有子节点,依次加入队列排队等待访问。
import java.util.*;
public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> res = new ArrayList();
if (root == null) {
return res;
}
Queue<TreeNode> q = new ArrayDeque<TreeNode>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
res.add(cur.val);
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
return res;
}
}
题目:
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true ,否则返回 false 。假设输入的数组的任意两个数字都互不相同。
数据范围: 节点数量0≤n≤1000 ,节点上的值满足1≤val≤10 ^5,保证节点上的值各不相同
要求:空间复杂度 O(n),时间时间复杂度 O(n^2)
提示:
1.二叉搜索树是指父亲节点大于左子树中的全部节点,但是小于右子树中的全部节点的树。
2.该题我们约定空树不是二叉搜索树
3.后序遍历是指按照 “左子树-右子树-根节点” 的顺序遍历
解析:
思路一:
BST的后序序列的合法序列是,对于一个序列S,最后一个元素是x (也就是root节点),如果去掉最后一个元素的序列为 T,那么T满足:T可以分成两段,前一段(左子树)小于x,后一段(右子树)大于x,且这两段(子树)都是合法的后序序列。
import java.util.*;
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
if (sequence.length == 0) {
return false;
}
return Core(sequence, 0, sequence.length - 1);
}
public boolean Core(int [] sequence, int start, int end) {
if (start >= end) {
return true;
}
int mid = sequence[end];
int i = start;
while (i < end && sequence[i] < mid) {
i++;
}
for (int j = i; j < end; j++) {
if (sequence[j] < mid ) {
return false;
}
}
return Core(sequence, start, i - 1) &&
Core(sequence, i, end - 1);
}
}
本来准备直接在原本的函数上进行递归,但是因为数组不太好操作,还是选择了别人讲解的方法。
思路二:
利用单调栈。(不会,待解决)。
题目:
输入一颗二叉树的根节点root和一个整数expectNumber,找出二叉树中结点值的和为expectNumber的所有路径。
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
解析:
import java.util.*;
import java.util.ArrayList;
public class Solution {
ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public void DFS(TreeNode root, int targrt) {
if (root == null) {
return ;
}
path.add(root.val);
targrt -= root.val;
if (root.left == null && root.right == null && targrt == 0) {
ret.add(new ArrayList<>(path));
}
DFS(root.left, targrt);
DFS(root.right, targrt);
// path.removeIf(p->(p == path.get(path.size() - 1)));
path.removeLast();
}
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int expectNumber) {
DFS(root, expectNumber);
return ret;
}
}
Java.util.LinkedList.removeLast()
方法:用于从LinkedList中删除最后一个元素。删除元素后,此方法还返回元素。
自己原本采用的是ArrayList在Java 8 中的.removeIf()
方法,该函数中中要填写的参数是布尔类型。自己写的条件是等于最后一个元素位置的值就删除,但是这样如果前面有元素也等于最后一个值也就一起删除了,所以错了。
题目:
输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。
例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。
解析:
import java.util.*;
public class Solution {
public String change(char[] a) {
StringBuilder res = new StringBuilder();
for (char value : a) {
res.append(value);
}
return res.toString();
}
public void solve(ArrayList<String> arr, char[] a, int index, int length) {
if (index == length - 1) {
String res = change(a);
arr.add(res);
} else {
for (int i = index; i < length; i++) {
char temp = a[i];
a[i] = a[index];
a[index] = temp;
solve(arr, a, index + 1, length);
temp = a[i];
a[i] = a[index];
a[index] = temp;
}
}
}
public ArrayList<String> Permutation (String str) {
char[] a = str.toCharArray();
ArrayList<String> arr = new ArrayList<>();
solve(arr, a, 0, str.length());
// 去重
arr = new ArrayList<String>(new HashSet<String>(arr));
// 排序
Collections.sort(arr);
return arr;
}
}
import java.util.*;
public class Solution {
public void Swap(char[] str, int i, int j) {
char temp = str[i];
str[i] = str[j];
str[j] = temp;
}
public boolean IsExist(ArrayList<String> result, char[] str) {
return result.contains(String.valueOf(str));
}
public void PermutationHelper(char[] str, int start,
ArrayList<String> result) {
if (start == str.length - 1) {
if (!IsExist(result, str)) {
result.add(new String(str));
}
return;
}
for (int i = start; i < str.length; i++) {
Swap(str, start, i);
PermutationHelper(str, start + 1, result);
Swap(str, start, i);
}
}
public ArrayList<String> Permutation(String str) {
ArrayList<String> result = new ArrayList<>();
if (str != null && str.length() > 0) {
PermutationHelper(str.toCharArray(), 0, result);
Collections.sort(result);
}
return result;
}
}
题目:
给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。
数据范围0≤k,n≤10000,数组中每个数的大小0≤val≤1000
要求:空间复杂度 O(n) ,时间复杂度 O(nlogn)
解析:
知识点:优先队列
优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是O(log2n)O(log_2n)O(log
2
n),而每次取出堆顶元素都是直接取出。
思路:
要找到最小的k个元素,只需要准备k个数字,之后每次遇到一个数字能够快速的与这k个数字中最大的值比较,每次将最大的值替换掉,那么最后剩余的就是k个最小的数字了。
如何快速比较k个数字的最大值,并每次替换成较小的新数字呢?我们可以考虑使用优先队列(大根堆),只要限制堆的大小为k,那么堆顶就是k个数字的中最大值,如果需要替换,将这个最大值拿出,加入新的元素就好了。
具体做法:
step 1:利用input数组中前k个元素,构建一个大小为k的大顶堆,堆顶为这k个元素的最大值。
step 2:对于后续的元素,依次比较其与堆顶的大小,若是比堆顶小,则堆顶弹出,再将新数加入堆中,直至数组结束,保证堆中的k个最小。
step 3:最后将堆顶依次弹出即是最小的k个数。
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if(input == null ||k<=0||k>input.length){
return list;
}
//按照数值从大到小
PriorityQueue<Integer> queue = new PriorityQueue<>(k, Collections.reverseOrder());
for(int i = 0; i < input.length;i++){
if(i < k){//前提前插入k个数据,queue会自动排序
queue.offer(input[i]);
} else{//否则,就要展开淘汰的过程了,每次都淘汰最大的,剩下的最终就是最小的k个
if(input[i] < queue.peek()){
queue.poll();
queue.offer(input[i]);
}
}
}
//返回对应的结果
for(int i = 0;i <k;i++){
list.add(queue.poll());
}
return list;
}
}
Collections.reverseOrder()
方法,该方法返回一个比较器,该比较器提供对实现Comparable接口的对象集合进行自然排序的逆序。
PriorityQueue
优先队列的大小是不受限制的,但在创建时可以指定初始大小。当我们向优先队列增加元素的时候,队列大小会自动增加。
add()
和offer()
:都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。对于PriorityQueue这两个方法其实没什么差别。element()
和peek()
:都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null。remove()
和poll()
:都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。题目:
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
数据范围:1<=n<=2×10^5 ;−100<=a[i]<=100
要求:时间复杂度为 O(n),空间复杂度为 O(n)
进阶:时间复杂度为 O(n),空间复杂度为 O(1)
解析:
知识点:动态规划
动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
思路:
因为数组中有正有负有0,因此每次遇到一个数,要不要将其加入我们所求的连续子数组里面,是个问题,有可能加入了会更大,有可能加入了会更小,而且我们要求连续的最大值,因此这类有状态转移的问题可以考虑动态规划。
import java.util.*;
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int [] dp = new int[array.length];
int max = array[0];
dp[0] = array[0];
for ( int i = 1; i < array.length; i++) {
dp[i] = Math.max(dp[i - 1] + array[i], array[i]);
max = Math.max(max, dp[i]);
}
return max;
}
}
题目:
解析:
题目:
输入一个非负整数数组numbers,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
例如输入数组[3,32,321],则打印出这三个数字能排成的最小数字为321323。
1.输出结果可能非常大,所以你需要返回一个字符串而不是整数
2.拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0
数据范围:
0<=len(numbers)<=100
解析:
step 1:优先判断空数组的特殊情况。
step 2:将数组中的数字元素转换成字符串类型。
step 3:重载排序比较为字符串类型的x + y < y + x,然后进行排序。
step 4:将排序结果再按照字符串拼接成一个整体。
import java.util.*;
public class Solution {
public String PrintMinNumber(int [] numbers) {
if (numbers == null || numbers.length == 0) {
return " ";
}
ArrayList<Integer> list = new ArrayList<Integer>();
for (int e : numbers) {
list.add(e);
}
Collections.sort(list, new Comparator<Integer>() {
public int compare(Integer x, Integer y) {
String xs = x + "" + y;
String ys = y + "" + x;
return xs.compareTo(ys);
}
});
StringBuffer result = new StringBuffer();
for (Integer e : list) {
result.append(e);
}
return result.toString();
}
}
解析:
思路一:
使用两个指针N1,N2,一个从链表1的头节点开始遍历,我们记为N1,一个从链表2的头节点开始遍历,我们记为N2。
让N1和N2一起遍历,当N1先走完链表1的尽头(为null)的时候,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2的尽头,则从链表1的头节点继续遍历,也就是说,N1和N2都会遍历链表1和链表2。
因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。
(N1最后肯定能到达链表2的终点,N2肯定能到达链表1的终点)。
所以,如何得到公共节点:
有公共节点的时候,N1和N2必会相遇,因为长度一样嘛,速度也一定,必会走到相同的地方的,所以当两者相等的时候,则会第一个公共的节点
无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是null,所以也算是相等了。
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode l1 = pHead1;
ListNode l2 = pHead2;
while (l1 != l2) {
l1 = (l1 == null) ? pHead2 : l1.next;
l2 = (l2 == null) ? pHead1 : l2.next;
}
return l1;
}
}
思路二:
题目要求是单链表,所以如果有交点,则最后一个链表的节点地址一定是相同的;
求第一公共节点,本质是让长的链表先走abs(length1-length2)步,后面大家的步调一致,往后找第一个地址相同的 节点,就是题目要求的节点。所以需要各自遍历两次链表。
public class Solution {
public int GetListLength(ListNode list) {
if (list == null) {
return 0;
}
int len = 0;
while (list != null) {
len++;
list = list.next;
}
return len;
}
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
int length1 = GetListLength(pHead1);
int length2 = GetListLength(pHead2);
int l12 = Math.abs(length1 - length2);
if ( length1 > length2) {
while (l12 > 0) {
pHead1 = pHead1.next;
l12--;
}
} else {
while (l12 > 0) {
pHead2 = pHead2.next;
l12--;
}
}
while (pHead1 != null && pHead2 != null) {
if (pHead1 == pHead2) {
return pHead1;
}
pHead1 = pHead1.next;
pHead2 = pHead2.next;
}
return null;
}
}
题目:
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度,根节点的深度视为 1 。
数据范围:节点的数量满足 0≤n≤100 ,节点上的值满足0≤val≤100
进阶:空间复杂度 O(1) ,时间复杂度 O(n)
解析:
思路一:利用队列和层次遍历
既然是统计二叉树的最大深度,除了根据路径到达从根节点到达最远的叶子节点以外,我们还可以分层统计。对于一棵二叉树而言,必然是一层一层的,那一层就是一个深度,有的层可能会很多节点,有的层如根节点或者最远的叶子节点,只有一个节点,但是不管多少个节点,它们都是一层。因此我们可以使用层次遍历,二叉树的层次遍历就是从上到下按层遍历,每层从左到右,我们只要每层统计层数即是深度。
具体做法:
step 1:既然是层次遍历,我们遍历完一层要怎么进入下一层,可以用队列记录这一层中节点的子节点。队列类似栈,只不过是一个先进先出的数据结构,可以理解为我们平时的食堂打饭的排队。因为每层都是按照从左到右开始访问的,那自然记录的子节点也是从左到右,那我们从队列出来的时候也是从左到右,完美契合。
step 2:在刚刚进入某一层的时候,队列中的元素个数就是当前层的节点数。比如第一层,根节点先入队,队列中只有一个节点,对应第一层只有一个节点,第一层访问结束后,它的子节点刚好都加入了队列,此时队列中的元素个数就是下一层的节点数。因此遍历的时候,每层开始统计该层个数,然后遍历相应节点数,精准进入下一层。
step 3:遍历完一层就可以节点深度就可以加1,直到遍历结束,即可得到最大深度。
import java.util.*;
public class Solution {
public int TreeDepth(TreeNode root) {
// 空
if (root == null)
return 0;
// 用一个队列存储每层节点(队列先进先出)
Queue<TreeNode> p = new LinkedList<TreeNode>();
// 首先将根节点入队列
p.offer(root);
// 记录层数
int res = 0;
while (!p.isEmpty()) {
int n = p.size();
// 遍历完这一层,再进入下一层
for (int i = 0; i < n; i++) {
// 队列输出
TreeNode node = p.poll();
// 若该节点有左右节点,就入队列
if (node.left != null) {
p.offer(node.left);
}
if (node.right != null) {
p.offer(node.right);
}
}
// 遍历完一层之后深度加1
res++;
}
return res;
}
}
思路二:递归
import java.util.*;
public class Solution {
public int maxDepth (TreeNode root) {
//空节点没有深度
if(root == null)
return 0;
//返回子树深度+1
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
题目:
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
解析:
思路一:利用栈
import java.util.*;
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
Arrays.sort(array);
Stack<Integer> sta = new Stack<Integer>();
for(int i = 0; i < array.length; i++) {
if(sta.empty() || sta.peek() != array[i]) {
sta.push(array[i]);
}else{
sta.pop();
}
}
num1[0] = sta.pop();
num2[0] = sta.pop();
}
}
思路二:
利用位运算
(不太懂)
题目:
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?
数据范围:0
返回值描述:
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
解析:
思路一:滑动窗口
从某一个数字开始的连续序列和等于目标数如果有,只能有一个,于是我们可以用这个性质来使区间滑动。
两个指针l、r指向区间首和区间尾,公式(l+r)∗(r−l+1)/2计算区间内部的序列和,如果这个和刚好等于目标数,说明以该区间首开始的序列找到了,记录下区间内的序列,同时以左边开始的起点就没有序列了,于是左区间收缩;如果区间和大于目标数,说明该区间过长需要收缩,只能收缩左边;如果该区间和小于目标数,说明该区间过短需要扩大,只能向右扩大,移动区间尾。
具体做法:
step 1:从区间[1,2]开始找连续子序列。
step 2:每次用公式计算区间内的和,若是等于目标数,则记录下该区间的所有数字,为一种序列,同时左区间指针向右。
step 3:若是区间内的序列和小于目标数,只能右区间扩展,若是区间内的序列和大于目标数,只能左区间收缩(在这之前已经判断过左区间在左移一个的位置结果,然后再判断左区间在当前位置的情况)。(一定要好好理解这两个只能)
import java.util.ArrayList;
public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> result = new ArrayList<>();
int low = 1;
int high = 2;
while (low < high) {
int temp = (low + high) * (high - low + 1) / 2;
if (temp == sum) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = low; i <= high; i++) {
list.add(i);
}
result.add(list);
low++;
} else if (temp < sum) {
high++;
} else {
low++;
}
}
return result;
}
}
思路二:枚举
从数字1开始枚举连续的数字,将其累加判断其是否等于目标,如果小于目标数则继续往后累加,如果大于目标数说明会超过,跳出,继续枚举下一个数字开始的情况(比如2,比如3),这样每次都取连续的序列,只有刚好累加和等于目标数才可以记录从开始到结束这一串数字,代表是一个符合的序列。
因为序列至少两个数,每次枚举区间的起始数字最多到目标数的一半向下取整即可,因为两个大于目标数一半的数相加一定大于目标数。
具体做法:
step 1:从1到目标值一半向下取整作为枚举的左区间,即每次序列开始的位置。
step 2:从每个区间首部开始,依次往后累加,如果大于目标值说明这一串序列找不到,换下一个起点。
step 3:如果加到某个数字刚好等于目标值,则记录从区间首到区间尾的数字。
import java.util.*;
public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer> >();
int sum1 = 0;
//因为序列至少两个数,因此枚举最多到该数字的一半向下取整
int up = (sum - 1) / 2;
//枚举左区间
for(int i = 1; i <= up; i++){
//从左区间往后依次连续累加
for(int j = i; ;j++){
sum1 += j;
//大于目标和则跳出该左区间
if(sum1 > sum){
sum1 = 0;
break;
//等于则找到
}else if(sum1 == sum){
sum1 = 0;
ArrayList<Integer> temp = new ArrayList<Integer>();
//记录线序的数字
for(int k = i; k <= j; k++)
temp.add(k);
res.add(temp);
break;
}
}
}
return res;
}
}
题目:
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列 S ,请你把其循环左移 K 位后的序列输出。例如,字符序列 S = ”abcXYZdef” , 要求输出循环左移 3 位后的结果,即 “XYZdefabc”
数据范围:输入的字符串长度满0≤len≤100 , 0≤n≤100
进阶:空间复杂度 O(n) ,时间复杂度 O(n)
解析:
思路一:拼接
既然循环左移是前面n个字符平移到了最后,我们就可以分开加入字符串中,先挨个加入后面部分字符,然后再回过头加入前面的字符。
具体做法:
step 1:因为n可能大于字符串长度,因此需要对长度m取余,因为每次长度为mmm的旋转相当于没有变化。(自己刚刚开始的时候,没有取余,只有部分测试用例可以通过,如果n大于字符串长度,则出现了越界的问题)
step 2:先遍历后m−n个字符,依次加入待返回的字符串中。
step 3:再遍历前nnn个字符,依次加入待返回的字符串中。
public class Solution {
public String LeftRotateString(String str, int n) {
if (str.isEmpty() || str.length() == 0) {
return "";
}
n = n % str.length();
StringBuilder res = new StringBuilder();
for (int i = n; i < str.length(); i++) {
res.append(str.charAt(i));
}
for (int i = 0; i < n; i++) {
res.append(str.charAt(i));
}
return res.toString();
}
}
思路二:三次反转
循环左移相当于从第nnn个位置开始,左右两部分视作整体翻转。即abcdefg左移3位defgabc可以看成AB翻转成BA(这里小写字母看成字符元素,大写字母看成整体)。既然是翻转我们就可以用到reverse函数。
具体做法:
step 1:因为n可能大于字符串长度,因此需要对长度m取余,因为每次长度为m的旋转相当于没有变化。
step 2:第一次将整个字符串翻转,得到字符串的逆序,它已经满足了左移的整体出现在了右边。
step 3:第二次就将左边的m−n个元素单独翻转,因为它虽然移到了左边,但是逆序了。
step 4:第三次就将右边的nnn个元素单独翻转,因此这部分也逆序了。
public class Solution {
public String LeftRotateString(String str, int n) {
if (str.isEmpty() || str.length() == 0) {
return "";
}
n = n % str.length();
char[] c = str.toCharArray();
reverse(c, 0, str.length() - 1);
reverse(c, 0, str.length()-n - 1);
reverse(c, str.length()-n, str.length() - 1);
return new String(c);
}
public void reverse(char[] c, int start, int end) {
while (start < end) {
char temp = c[start];
c[start] = c[end];
c[end] = temp;
start++;
end--;
}
}
}
注意下面几个位置中,reverse函数的传入参数的值,特的是第二个start和第三个end。
reverse(c, 0, str.length() - 1);
reverse(c, 0, str.length()-n - 1);
reverse(c, str.length()-n, str.length() - 1);
题目:
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“nowcoder. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a nowcoder.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
数据范围:1≤n≤100
进阶:空间复杂度 O(n),时间复杂度 O(n) ,保证没有只包含空格的字符串
解析:
思路一:
import java.util.*;
public class Solution {
public String ReverseSentence(String str) {
String[] str1=str.trim().split(" ");
StringBuilder res = new StringBuilder();
for(int i=str1.length-1;i>=0;i--){
res.append(str1[i]+" ");
}
return res.toString().trim();
}
}
str.trim()
用于删除字符串的头尾空白符,空白符包括:空格、制表符 tab、换行符等其他空白符等。
思路二:反转
我们需要的是将单词位置反转,也即是单词内部不变,属于字符串部分反转问题。如果将整个字符串反转,单词位置倒是反转了,但是内部次序也改变了,此时就需要将内部再反转回去,因此两次反转可以解决。
具体做法:
step 1:将字符串整体反转。
step 2:遍历反转后的字符串,以空格为界限找到一个单词。
step 3:将每个单词部分反转。
import java.util.*;
public class Solution {
public String ReverseSentence(String str) {
int n = str.length();
char[] c = str.toCharArray();
reverse(c, 0, n - 1);
for (int i = 0; i < n; i++) {
int j = i;
while (j < n && c[j] != ' ') {
j++;
}
reverse(c, i, j - 1);
i = j;
}
return new String(c);
}
public void reverse(char[] c, int start, int end) {
while (start < end) {
char temp = c[start];
c[start] = c[end];
c[end] = temp;
start++;
end--;
}
}
}
思路三:利用栈
import java.util.*;
public class Solution {
public String ReverseSentence(String str) {
Stack<String> st = new Stack<String>();
String[] temp = str.split(" ");
//单词加入栈中
for(int i = 0; i < temp.length; i++){
st.push(temp[i]);
st.push(" ");
}
StringBuilder res = new StringBuilder();
//去掉最后一个空格
if(!st.isEmpty())
st.pop();
//栈遵循先进后厨,单词顺序是反的
while(!st.isEmpty())
res.append(st.pop());
return res.toString();
}
}
题目:
给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替)
数据范围:0≤n≤1500,树上每个节点的val满足0∣val∣<=1500
要求:空间复杂度:O(n),时间复杂度:O(n)
解析:
思路一:
step 1:首先判断二叉树是否为空,空树没有打印结果。
step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面,初始化flag变量。
step 3:每次进入一层,统计队列中元素的个数,更改flag变量的值。因为每当访问完一层,下一层作为这一层的子节点,一定都加入队列,而再下一层还没有加入,因此此时队列中的元素个数就是这一层的元素个数。
step 4:每次遍历这一层这么多的节点数,将其依次从队列中弹出,然后加入这一行的一维数组中,如果它们有子节点,依次加入队列排队等待访问。
step 5:访问完这一层的元素后,根据flag变量决定将这个一维数组直接加入二维数组中还是反转后再加入,然后再访问下一层。
import java.util.*;
import java.util.ArrayList;
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
TreeNode head = pRoot;
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
if (head == null) {
return res;
}
Queue<TreeNode> temp = new LinkedList<TreeNode>();
temp.offer(head);
TreeNode p;
boolean flag = true;
while (!temp.isEmpty()) {
ArrayList<Integer> row = new ArrayList<Integer>();
int n = temp.size();
flag = !flag;
for (int i = 0; i < n; i++) {
p = temp.poll();
row.add(p.val);
if (p.left != null) {
temp.offer(p.left);
}
if (p.right != null) {
temp.offer(p.right);
}
}
if (flag) {
Collections.reverse(row);
}
res.add(row);
}
return res;
}
}
题目:
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
解析:
import java.util.Stack;
//BST本身就是有序的,中序遍历即是升序
//要求第k小,即中序遍历时到达第k个元素(二叉搜索树,不存在两个相同的节点值)
//此处,我们不使用递归,我们采用循环中序遍历的方式
public class Solution {
TreeNode KthNode(TreeNode pRoot, int k) {
if(pRoot == null || k <= 0){
return null;
}
Stack<TreeNode> st = new Stack<>();
TreeNode node = pRoot;
do{
while(node != null){
st.push(node);
node = node.left;
}
if(!st.empty()) {
node = st.pop();
//访问当前节点,中序
k--;
if(k == 0){
return node;//找到当前第k小节点
}
node = node.right;
}
}while(node != null || !st.empty()); //node有可能为空,但是只要stack不为空,就要继续pop求 下一个。有没有可能st为空?有可能,这个时候就要检测node,如果node不为空,就要整体检测右子树
//走到这里,就说明没有找到
return null;
}
}
解析:
import java.util.*;
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
}
题目:
给定一个链表,首先判断其是否有环,然后找到环的入口
解析:
思路一:利用双指针
import java.util.*;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode slow = hasCycle(pHead);
if (slow == null) {
return null;
}
ListNode fast = pHead;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
public ListNode hasCycle(ListNode pHead) {
ListNode fast = pHead;
ListNode slow = pHead;
if ( pHead == null) {
return null;
}
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return fast;
}
}
return null;
}
}
思路二:利用HashSet,
import java.util.*;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
HashSet<ListNode> set = new HashSet<>();
while (pHead != null) {
if (set.contains(pHead)) {
return pHead;
}
set.add(pHead);
pHead = pHead.next;
}
return null;
}
}
题目:
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)。 下图是一个含有5个结点的复杂链表。图中实线箭头表示next指针,虚线箭头表示random指针。为简单起见,指向null的指针没有画出。
解析:
参考视频:https://www.bilibili.com/video/BV1mf4y1E7qv?vd_source=e5a575003d205f54dc71d7815361a220
看代码中的注解
也要注意源代码中关于RandomListNode这个类的注解。以前总是忽略。
import java.util.*;
/*
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
*/
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
// 边界判断
if (pHead == null) {
return null;
}
// 使用一一对应的HashMap结构存放已经创建的节点
// key:原始链表中的节点
// value:新链表中的节点
HashMap<RandomListNode, RandomListNode> map = new HashMap<>();
// 从链表的头结点开始遍历
RandomListNode cur = pHead;
// 遍历原链表
while (cur != null) {
// 不断新建空间存放新链表节点
RandomListNode newNode = new RandomListNode(cur.label);
// 此时key是原始链表中的节点,该节点有next和random指针
// 此时value代表复制的新节点,目前还没有next和random指针,需要后面再一次遍历时处理
map.put(cur, newNode);
// 遍历下一个节点
cur = cur.next;
}
// 再次从链表的头结点开始遍历
cur = pHead;
while (cur != null) {
// 0. 从字典中找到一个cur为key的那个对应的value值
RandomListNode new_cur = map.get(cur);
// 1. 接下来为new_cur寻找next和random节点
// 1.1 寻找next节点
// 1.1.1 获取当前节点cur在原始链表中的next指针指向的节点cur_next
RandomListNode cur_next = cur.next;
// 1.1.2 在字典中找到以cur_next为key的value值
RandomListNode new_next = map.get(cur_next);
// 1.1.3 那么在新链表中以new_cur为节点的next指针就是new_next
new_cur.next = new_next;
// 1.2 寻找random节点
// 1.2.1 获取当前节点cur在原始链表中的random指针指向的节点cur_random
RandomListNode cur_random = cur.random;
// 1.2.2在字典中找到以cur_random为key的value值
RandomListNode new_random = map.get(cur_random);
// 1.2.3那么在新链表中以new_cur为节点的random指针就是new_next
new_cur.random = new_random;
// 遍历下一个节点
cur = cur.next;
}
// 返回头结点
return map.get(pHead);
}
}
解析:
https://www.nowcoder.com/practice/65cfde9e5b9b4cf2b6bafa5f3ef33fa6?tpId=295&sfm=html&channel=nowcoder
思路一:利用分治+合并两个链表
import java.util.*;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
return devideMerge(lists, 0, lists.size() - 1);
}
public ListNode devideMerge(ArrayList<ListNode> lists, int left, int right) {
if (left > right) {
return null;
} else if (left == right) {
return lists.get(left);
}
int mid = ( left + right ) / 2;
return merge(devideMerge(lists, left, mid), devideMerge(lists, mid + 1, right));
}
public ListNode merge(ListNode list1, ListNode list2) {
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
ListNode list = new ListNode(0);
ListNode cur = list;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
cur = cur.next;
} else {
cur.next = list2;
list2 = list2.next;
cur = cur.next;
}
}
if (list1 != null) {
cur.next = list1;
}
if (list2 != null) {
cur.next = list2;
}
return list.next;
}
}
思路二:利用优先队列
import java.util.*;
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
//小顶堆 创建优先队列,设置比较器为队列节点值从小到大排列
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
//遍历所有链表第一个元素
for(int i = 0; i < lists.size(); i++){
//不为空则加入小顶堆
if(lists.get(i) != null)
pq.add(lists.get(i));
}
//加一个表头
ListNode res = new ListNode(-1);
ListNode head = res;
//直到小顶堆为空
while(!pq.isEmpty()){
//取出最小的元素
ListNode temp = pq.poll();
//连接
head.next = temp;
head = head.next;
//每次取出链表的后一个元素加入小顶堆
if(temp.next != null)
pq.add(temp.next);
}
//去掉表头
return res.next;
}
}
new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
堆的初始化,实现小顶堆,如同:
Queue queue = new PriorityQueue<>(new Comparator() {
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});
解析:
利用头插法迭代
step 1:我们可以在链表前加一个表头,后续返回时去掉就好了,因为如果要从链表头的位置开始反转,在多了一个表头的情况下就能保证第一个节点永远不会反转,不会到后面去。
step 2:使用两个指针,一个指向当前节点,一个指向前序节点。
step 3:依次遍历链表,到第m个的位置。
step 4:对于从m到n这些个位置的节点,依次断掉指向后续的指针,反转指针方向。
step 5:返回时去掉我们添加的表头。
import java.util.*;
public class Solution {
/**
*
* @param head ListNode类
* @param m int整型
* @param n int整型
* @return ListNode类
*/
public ListNode reverseBetween (ListNode head, int m, int n) {
// write code here
ListNode res = new ListNode(-1);
res.next = head;
ListNode pre = res;
ListNode cur = head;
for (int i = 1; i < m; i++) {
pre = cur;
cur = cur.next;
}
for (int i = m; i < n; i++) {
ListNode temp = cur.next;
cur.next = temp.next;
temp.next = pre.next;
pre.next = temp;
}
return res.next;
}
}
解析:
参考 视频讲解:https://www.bilibili.com/video/BV1i44111721?vd_source=e5a575003d205f54dc71d7815361a220
CSDN:https://blog.csdn.net/qq_28410301/article/details/100267021
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @param k int整型
* @return ListNode类
*/
public ListNode reverseKGroup (ListNode head, int k) {
// write code here
ListNode res = new ListNode(0);
res.next = head;
ListNode pre = res;
ListNode start = head;
ListNode end = head;
ListNode next = head;
while (next != null ) {
for (int i = 1; i < k && end != null ; i++) {
end = end.next;
}
if (end == null) {
break;
}
next = end.next;
end.next = null;
end = start;
start = reverse(start);
end.next = next;
pre.next = start;
pre = end;
start = next;
end = start;
}
return res.next;
}
public ListNode reverse(ListNode head) {
// 新链表
ListNode newHead = null;
while (head != null) {
ListNode temp = head.next;
head.next = newHead;
newHead = head;
head = temp;
}
return newHead;
}
}
解析:
思路一:先计算出链表长度。
step 1:给链表添加一个表头,处理删掉第一个元素时比较方便。
step 2:遍历整个链表,统计链表长度。
step 3:准备两个指针,一个指向原始链表头,表示当前节点,一个指向我们添加的表头,表示前序节点。两个指针同步遍历L−nL-nL−n次找到倒数第nnn个位置。
step 4:前序指针的next指向当前节点的next,代表越过当前节点连接,即删去该节点。
step 5:返回去掉添加的表头。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @param n int整型
* @return ListNode类
*/
public ListNode removeNthFromEnd (ListNode head, int n) {
ListNode res = new ListNode(-1);
res.next = head;
ListNode pre = res;
ListNode low = head;
int len = 1;
while (low != null) {
low = low.next;
len++;
}
low = head;
for (int i = 1; i < len - n ; i++) {
pre = low;
low = low.next;
}
pre.next = low.next;
return res.next;
}
}
思路二:双指针
step 1:给链表添加一个表头,处理删掉第一个元素时比较方便。
step 2:准备一个快指针,在链表上先走n步。
step 3:准备慢指针指向原始链表头,代表当前元素,前序节点指向添加的表头,这样两个指针之间相距就是一直都是n。
step 4:快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数nnn个元素的位置。
step 5:最后将该节点前序节点的指针指向该节点后一个节点,删掉这个节点。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @param n int整型
* @return ListNode类
*/
public ListNode removeNthFromEnd (ListNode head, int n) {
ListNode res = new ListNode(-1);
res.next = head;
ListNode pre = res;
ListNode fast = head;
ListNode cur = head;
while (n != 0) {
fast = fast.next;
n--;
}
while (fast != null) {
fast = fast.next;
pre = cur;
cur = cur.next;
}
pre.next = cur.next;
return res.next;
}
}
解析:
step 1:任意一个链表为空,返回另一个链表就行了,因为链表为空相当于0,0加任何数为0,包括另一个加数为0的情况。
step 2:相继反转两个待相加的链表,反转过程可以参考反转链表。
step 3:设置返回链表的链表头,设置进位carry=0.
step 4:从头开始遍历两个链表,直到两个链表节点都为空且carry也不为1. 每次取出不为空的链表节点值,为空就设置为0,将两个数字与carry相加,然后查看是否进位,将进位后的结果(对10取模)加入新的链表节点,连接在返回链表后面,并继续往后遍历。
step 5:返回前将结果链表再反转回来。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head1 ListNode类
* @param head2 ListNode类
* @return ListNode类
*/
public ListNode addInList (ListNode head1, ListNode head2) {
if (head1 == null) {
return head2;
}
if (head2 == null) {
return head1;
}
head1 = reverse(head1);
head2 = reverse(head2);
ListNode res = new ListNode(-1);
ListNode head = res;
int carry = 0;
while (head1 != null || head2 != null || carry != 0) {
int val1 = ((head1 == null) ? 0 : head1.val);
int val2 = ((head2 == null) ? 0 : head2.val);
int temp = val1 + val2 + carry;
carry = temp / 10;
temp %= 10;
head.next = new ListNode(temp);
head = head.next;
if (head1 != null) {
head1 = head1.next;
}
if (head2 != null) {
head2 = head2.next;
}
}
return reverse(res.next);
}
public ListNode reverse(ListNode head) {
if (head == null) {
return null;
}
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
ListNode pre = null;
和ListNode res = new ListNode(-1)
使用区别?在代码中两处总是写错。
题目:
给定一个节点数为n的无序单链表,对其按升序排序。
数据范围:0
解析:
思路一:转换为数组,然后排序
step 1:遍历链表,将节点值加入数组。
step 2:使用内置的排序函数对数组进行排序。
step 3:依次遍历数组和链表,按照位置将链表中的节点值修改为排序后的数组值。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类 the head node
* @return ListNode类
*/
public ListNode sortInList (ListNode head) {
// write code here
ArrayList<Integer> list = new ArrayList<>();
ListNode res = head;
while (res != null) {
list.add(res.val);
res = res.next;
}
Collections.sort(list);
res = head;
for(int i = 0; i < list.size();i++){
res.val = list.get(i);
res=res.next;
}
return head ;
}
}
思路二:分治加双指针
前面我们做合并两个有序链表不是使用归并思想吗?说明在链表中归并排序也不是不可能使用,合并阶段可以参照前面这道题,两个链表逐渐取最小的元素就可以了,但是划分阶段呢?
常规数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。而链表中我们也可以用同样的方式,只需要找到中间个元素的前一个节点,将其断开,就可以将链表分成两个子链表,然后继续划分,直到最小,然后往上依次合并。
终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
返回值: 每次返回两个排好序且合并好的子链表。
本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
怎么找中间元素呢?我们也可以使用快慢双指针,快指针每次两步,慢指针每次一步,那么快指针到达链表尾的时候,慢指针正好走了快指针距离的一半,为中间元素。
具体做法:
step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
step 4:将子问题得到的链表合并,参考合并两个有序链表。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类 the head node
* @return ListNode类
*/
public ListNode sortInList (ListNode head) {
// 链表为空或者只有一个元素,直接就是有序的
if (head == null || head.next == null) {
return head;
}
ListNode left = head;
ListNode mid = head.next;
ListNode right = head.next.next;
// 右边的指针到达末尾时,中间的指针指向该段链表的中间
while (right != null && right.next != null) {
left = left.next;
mid = mid.next;
right = right.next.next;
}
// 左边指针指向左段的左右一个节点,从这里断开
left.next = null;
// 分成两段排序,合并排好序的两段
return merge(sortInList(head), sortInList(mid));
}
public ListNode merge(ListNode list1, ListNode list2) {
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
// 加表头
ListNode head = new ListNode(-1);
ListNode cur = head;
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;
}
if (list2 != null) {
cur.next = list2;
}
return head.next;
}
}
题目:
给定一个链表,请判断该链表是否为回文结构。
回文是指该字符串正序逆序完全一致。
解析:
利用双指针
step 1:遍历一次链表,将元素取出放入辅助数组中。
step 2:使用下标访问,两个下标代表两个指针,两个指针分别从数组首尾开始遍历,左指针指向开头,从左到右,右指针指向数组末尾,从右到左,依次比较元素是否相同。
step 3:如果有不一样,则不是回文结构。否则遍历到两个指针相遇就好了,因为左指针到了右半部分都是右指针走过的路,比较的值也是与之前相同的。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类 the head
* @return bool布尔型
*/
public boolean isPail (ListNode head) {
ArrayList<Integer> nums = new ArrayList();
while (head != null) {
nums.add(head.val);
head = head.next;
}
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int x = nums.get(left);
int y = nums.get(right);
if (x != y)
return false;
left++;
right--;
}
return true;
}
}
解析:
第一个节点是奇数位,第二个节点是偶数,第二个节点后又是奇数位,因此可以断掉节点1和节点2之间的连接,指向节点2的后面即节点3,如红色箭头。如果此时我们将第一个节点指向第三个节点,就可以得到那么第三个节点后为偶数节点,因此我们又可以断掉节点2到节点3之间的连接,指向节点3后一个节点即节点4,如蓝色箭头。那么我们再将第二个节点指向第四个节点,又回到刚刚到情况了。
具体做法:
step 1:判断空链表的情况,如果链表为空,不用重排。
step 2:使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。
step 3:上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。
step 4:将偶数节点头接在奇数最后一个节点后,再返回头部。
import java.util.*;
public class Solution {
public ListNode oddEvenList (ListNode head) {
if (head == null) {
return head;
}
ListNode old = head;
ListNode even = head.next;
ListNode evenhead = even;
while (even != null && even.next != null) {
old.next = even.next;
old = old.next;
even.next = old.next;
even = even.next;
}
old.next = evenhead;
return head;
}
}
题目:
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
例如:
给出的链表为1→1→2,返回1→2.
给出的链表为1→1→2→3→3,返回1→2→3.
数据范围:链表长度满足0≤n≤100,链表中任意节点的值满足 ∣val∣≤100
进阶:空间复杂度 O(1),时间复杂度 O(n)
解析:
遍历删除
step 1:判断链表是否为空链表,空链表不处理直接返回。
step 2:使用一个指针遍历链表,如果指针当前节点与下一个节点的值相同,我们就跳过下一个节点,当前节点直接连接下个节点的后一位。
step 3:如果当前节点与下一个节点值不同,继续往后遍历。
step 4:循环过程中每次用到了两个节点值,要检查连续两个节点是否为空。
import java.util.*;
public class Solution {
public ListNode deleteDuplicates (ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
解析:
思路一:
step 1:给链表前加上表头,方便可能的话删除第一个节点。
step 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。(这是和上一题不同之处)
step 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
step 4:返回时去掉添加的表头。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
if (head == null) {
return null;
}
ListNode res = new ListNode(-1);
res.next = head;
ListNode cur = res;
while ( cur.next != null && cur.next.next != null) {
// 遇到相邻两个节点值相同
if (cur.next.val == cur.next.next.val) {
int temp = cur.next.val;
// 将所有相同的都跳过
while (cur.next != null && cur.next.val == temp) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return res.next;
}
}
思路二:哈希表
这道题幸运的是链表有序,我们可以直接与旁边的元素比较,然后删除重复。那我们扩展一点,万一遇到的链表无序呢?我们这里给出一种通用的解法,有序无序都可以使用,即利用哈希表来统计是否重复。
具体做法:
step 1:遍历一次链表用哈希表记录每个节点值出现的次数。
step 2:在链表前加一个节点值为0的表头,方便可能的话删除表头元素。
step 3:再次遍历该链表,对于每个节点值检查哈希表中的计数,只留下计数为1的,其他情况都删除。
step 4:返回时去掉增加的表头。
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode deleteDuplicates (ListNode head) {
if (head == null) {
return null;
}
HashMap<Integer, Integer> mp = new HashMap<>();
ListNode cur = head;
while (cur != null) {
if (mp.containsKey(cur.val)) {
mp.put(cur.val, mp.get(cur.val) + 1);
} else {
mp.put(cur.val, 1);
}
cur = cur.next;
}
ListNode res = new ListNode(-1);
res.next = head;
cur = res;
while (cur.next != null) {
if (mp.get(cur.next.val) != 1) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return res.next;
}
}
解析:
思路一:分别遍历来两个字符串,将其在点号之间的数字变成数字比较。
step 1:利用两个指针表示字符串的下标,分别遍历两个字符串。
step 2:每次截取点之前的数字字符组成数字,即在遇到一个点之前,直接取数字,加在前面数字乘10的后面。(因为int会溢出,这里采用long记录数字)
step 3:然后比较两个数字大小,根据大小关系返回1或者-1,如果全部比较完都无法比较出大小关系,则返回0.
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 比较版本号
* @param version1 string字符串
* @param version2 string字符串
* @return int整型
*/
public int compare (String version1, String version2) {
int n1 = version1.length();
int n2 = version2.length();
int i = 0, j = 0;
while (i < n1 || j < n2) {
long num1 = 0, num2 = 0;
while ( i < n1 && version1.charAt(i) != '.') {
num1 = num1 * 10 + (version1.charAt(i) - '0');
i++;
}
i++;
while ( j < n2 && version2.charAt(j) != '.') {
num2 = num2 * 10 + (version2.charAt(j) - '0');
j++;
}
j++;
if (num1 > num2)
return 1;
if (num1 < num2)
return -1;
}
return 0;
}
}
注意
这里的long num1 = 0, num2 = 0;
要放在while循环内部,因为每次比较都是比较两个点之间的数字,如果不放在while循环内部那么就成了将整个字符串变成对应数字,那1.0和1.0.0就会判断错误。