https://www.nowcoder.com/ta/classic-code 牛客上经典必刷题库
https://www.nowcoder.com/practice/e08819cfdeb34985a8de9c4e6562e724?tpId=46&tqId=29030&rp=1&ru=/ta/classic-code&qru=/ta/classic-code&difficulty=&judgeStatus=&tags=/question-ranking
题目CC1:(知识点:树,难度:较难)
求给定二叉树的最小深度。最小深度是指树的根结点到最近叶子结点的最短路径上结点的数量。
示例1
输入:
{1,2,3,4,5}
返回值:
2
代码:
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param root TreeNode类
* @return int整型
*/
public int run (TreeNode root) {
// write code here
if (root == null) {
return 0;
}
int left = run(root.left);
int right = run(root.right);
if (left * right > 0){
return (left > right ? right : left) + 1;
} else {
// 理解最小深度出现偏差,对根节点,左右节点两个为空(均返回0),或其中一个为空(则left或right为0,此时要取较大值,为0的值没有叶子节点不予考虑。根节点到最近叶子节点路径上的节点数。这个概念要理解清楚,否则编程出大问题。)
return (left > right ? left : right) + 1;
}
}
}
https://www.nowcoder.com/practice/22f9d7dd89374b6c8289e44237c70447?tpId=46&rp=1&ru=%2Fta%2Fclassic-code&qru=%2Fta%2Fclassic-code&difficulty=&judgeStatus=&tags=&title=&sourceUrl=&gioEnter=menu
题目CC2:(知识点:栈, 难度:较难)
计算逆波兰式(后缀表达式)的值
运算符仅包含"+“,”-“,”*“和”/",被操作数是整数
保证表达式合法,除法时向下取整。
数据范围:表达式的长度满足: n<10000
进阶:空间复杂度 O(n) , 时间复杂度 O(n)
例如:
[“20”, “10”, “+”, “30”, “*”] -> ((20 + 10) * 30) -> 900
[“40”, “130”, “50”, “/”, “+”] -> (40 + (130 / 50)) -> 42
示例1
输入:
[“20”,“10”,“+”,“30”,“*”]
返回值:
900
示例2
输入:
[“20”]
返回值:
20
代码:
import java.util.*;
public class Solution {
/**
*
* @param tokens string字符串一维数组
* @return int整型
*/
public int evalRPN (String[] tokens) {
// write code here
Stack<Integer> stack = new Stack<>();
int num1, num2;
for (int i = 0; i < tokens.length; i++) {
switch (tokens[i]) {
case "+":
num2 = stack.pop();
num1 = stack.pop();
stack.push(num1 + num2);
break;
case "-":
num2 = stack.pop();
num1 = stack.pop();
stack.push(num1 - num2);
break;
case "*":
num2 = stack.pop();
num1 = stack.pop();
stack.push(num1 * num2);
break;
case "/":
num2 = stack.pop();
num1 = stack.pop();
stack.push(num1 + num2);
break;
default:
stack.push(Integer.parseInt(tokens[i]));
}
}
return stack.pop();
}
}
https://www.nowcoder.com/practice/bfc691e0100441cdb8ec153f32540be2?tpId=46&rp=1&ru=%2Fta%2Fclassic-code&qru=%2Fta%2Fclassic-code&difficulty=&judgeStatus=&tags=&title=&sourceUrl=&gioEnter=menu
题目CC3:(知识点:穷举,难度:困难)
对于给定的n个位于同一二维平面上的点,求最多能有多少个点位于同一直线上
示例1
输入:
[(0,0),(0,1)]
返回值:
2
示例2
输入:
[(2,3),(3,3),(-5,3)]
返回值:
3
代码:
import java.util.*;
/*
* public class Point {
* int x;
* int y;
* }
*/
public class Solution {
/**
*
* @param points Point类一维数组
* @return int整型
*/
public int maxPoints (Point[] points) {
// write code here
if (points == null || points.length == 0) {
return 0;
}
if (points.length <= 2) {
return points.length;
}
//上面考虑特殊情况。下面就是通用情况考虑
int max = 0; // max存储最大值sum
for (int i = 0; i < points.length; i++) {
for (int j = i + 1; j < points.length; j++) {
// 两点形成一条直线,穷尽两个点的举例, 但会存在重复的直线,这里存在这个重复计算的问题,理论上可以优化。
//points[i] 和 points[j] 构成一条直线,重新遍历所有点,有多少点在该直线上,并将数据存在sum中。
int sum = 0;
if (points[i].x == points[j].x) {
// 考虑直线斜率无穷大的情况,所有点横坐标相同。
for (int k = 0; k < points.length; k++) {
if (points[k].x == points[i].x) {
sum++;
}
}
} else {
// 如何判断点在一条有斜率的直线上???
// y = kx + b // k, b体现直线的特征(不适用垂直于x轴的直线)
// 在程序中直接求k值(除不尽为小数)会存在精度损失的情况,所以必须采用某种表达式判断点是否在直线上。假设两个个点(x1,y1),(x2,y2),判断第三个点(x3,y3)(不同于前面两个点)是否在(x1,y1),(x2,y2)构成的直线上??我们知道(y2-y1)/(x2-x1) = (y3 - y2)/(x3 -x2)成立则第三个点肯定在直线上,对这个表达式变换一下:(y2-y1)*(x3-x2) = (y3-y2)*(x2-x1),程序中判断这个表达式就不会存在精度问题。本题中横纵坐标均是整型。若(x3,y3)为(x1,y1),(x2,y2)中的一个点,表达式依然成立
for (int m = 0; m < points.length; m++) {
if ((points[j].y - points[i].y) * (points[m].x - points[j].x) == (points[m].y - points[j].y) * (points[j].x - points[i].x)){
sum++;
}
}
}
// 遍历所有的每条直线产生的sum值。
if (sum > max) {
max = sum;
}
}
}
return max;
}
}
总结:穷举类型的题目在于用程序模拟求解的过程。本题要求最多有多少个点在一条直线上。所以考虑穷举所有的直线,并求每条直线上的点的个数,即可求得本题的解。
https://www.nowcoder.com/practice/d75c232a0405427098a8d1627930bea6?tpId=46&rp=1&ru=%2Fta%2Fclassic-code&qru=%2Fta%2Fclassic-code&difficulty=&judgeStatus=&tags=&title=&sourceUrl=&gioEnter=menu
题目CC4:(知识点:链表,排序,难度:较难)
在O(n log n)的时间内使用常数级空间复杂度对链表进行排序。
示例1
输入:
{30,20,40}
返回值:
{20,30,40}
本题思路:采用插入排序,但插入排序的时间复杂度为O(n^2),但本题采用插入排序,可以通过牛客网的全部测试用例。对于单链表而言,肯定采用插入排序更好。快排时间复杂度可以达到O(nlog2).对于排序,各种方法还需要好好复习,加以体会。
代码:
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode sortList (ListNode head) {
// write code here
if (head == null || head.next == null) {
return head;
}
//插入排序
ListNode p = head.next;//遍历剩下的链表元素
head.next = null; //用于构建插入排序链表,寻找合适位置并插入
while (p != null) {
ListNode r = p.next; //保存p的后续,以免链断了。
if (p.val < head.val) { //考虑特殊情况,首结点,p作为首节点,//这里若引入头结点就不用考虑特殊情况,头结点不存储任何数据,只存储指向链表首结点的指针。单链表插入操作必须考虑前驱节点。
p.next = head;
head = p;
p = r;
} else {
ListNode q = head;
//后面比较除head节点之外的节点
while (q.next != null && p.val >= q.next.val) {
q = q.next;
}
//找到合适位置并插入,p的值比q的下一个值小则p插入到q的后面, 或者遍历到最后q.next=null,则q插入到q的后面. 这里是单链表的插入操作,我竟然都忘记了,要维护两个指针,而我只写了q.next = p.
p.next = q.next;
q.next = p;
p = r;
}
}
return head;
}
}
本题第二种思路:为满足时间为O(nlogn),采用归并排序。冒泡排序、插入排序、选择排序这三种算法的时间复杂度都为O(n^2) ,只适合小规模的数据。两种时间复杂度为nlogn的排序算法——归并排序(Merge Sort)和快速排序(Quick Sort),他们都用到了分治思想,非常巧妙。参考网址:https://zhuanlan.zhihu.com/p/95080265
归并排序算法时间复杂度分析,参考网址:https://zhuanlan.zhihu.com/p/341225128
像这种排序问题的时空复杂度分析,还是要作比较深入的理解分析,对于自己的算法编程肯定大有益处。
使用快慢指针拆分两个链表,对两个链表分别递归排序,最终将排好序的两个链表归并排序成一个有序的链表,归并 排序需要额外的空间O(n)
代码:
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode sortList (ListNode head) {
// write code here
if (head == null) {
return null;
}
if (head.next == null) {
return head;
}
ListNode preSlow = head; // 慢指针指向元素的前一个元素
ListNode slow = head; // 慢指针
ListNode fast = head; // 快指针
while (slow != null && fast != null && fast.next != null) {
preSlow = slow;
slow = slow.next;
fast = fast.next.next;
}
// 循环结束后,slow指向链表中间的元素(元素个数为奇数),中间两个元素的后一个(元素个数为偶数)
ListNode listA = head; //指向前一个半链
ListNode listB = slow; //指向后一个半链。
preSlow.next = null; //将两个链断开。
listA = sortList(listA); // 对前一个半链递归分解合并排序
listB = sortList(listB); // 对后一个半链递归分解合并排序
return merge(listA, listB); // 将有序的listA 和有序的listB合并排序
}
private ListNode merge(ListNode listA, ListNode listB) {
ListNode dummy = new ListNode(0); // 作为临时链表的头节点,不存储实际内容
ListNode nodeA = listA;
ListNode nodeB = listB;
ListNode nodeR = dummy; // 指向临时链表的尾结点。
while (nodeA != null && nodeB != null) {
if (nodeA.val < nodeB.val) {
nodeR.next = nodeA;
nodeA = nodeA.next;
} else {
nodeR.next = nodeB;
nodeB = nodeB.next;
}
//每次插入一个,维护nodeR指针指向尾结点。
nodeR = nodeR.next;
}
if (nodeA != null) {
// nodeA节点后面有剩余元素,则一并拼接为一条链,不需要逐个添加。
nodeR.next = nodeA;
}
if (nodeB != null) {
nodeR.next = nodeB;
}
return dummy.next;
}
}
https://www.nowcoder.com/practice/152bc6c5b14149e49bf5d8c46f53152b?tpId=46&rp=1&ru=%2Fta%2Fclassic-code&qru=%2Fta%2Fclassic-code&difficulty=&judgeStatus=&tags=&title=&sourceUrl=&gioEnter=menu
题目CC5:(知识点:链表,排序,难度:较难)
使用插入排序对链表进行排序。
示例1
输入:
{30,20,40}
返回值:
{20,30,40}
代码:
import java.util.*;
/*
* public class ListNode {
* int val;
* ListNode next = null;
* }
*/
public class Solution {
/**
*
* @param head ListNode类
* @return ListNode类
*/
public ListNode insertionSortList (ListNode head) {
// write code here
if (head == null || head.next == null) {
return head;
}
// 至少两个节点
ListNode p = head.next; // 遍历p链表的每个元素,在有序链表head中寻找插入位置并插入。
head.next = null; //初始化head链表,只有一个链表有序,后面扩充该有序表。
while(p != null) {
ListNode q = head; // q用来遍历有序链表
ListNode r = p.next; // p插入有序表中会修改p.next,所以这里要保存。
if (p.val < q.val) {
//考虑特殊情况,插入位置为首结点前面。
p.next = q;
head = p;
p = r;
} else {
while (q.next != null && p.val >= q.next.val) {
q = q.next;
}
p.next = q.next;
q.next = p;
p = r;
}
}
return head;
}
}
题目CC6:(知识点:二叉树,难度:中等)
用递归的方法对给定的二叉树进行后序遍历。
例如:
给定的二叉树为{1,#,2,3},
返回[3,2,1].
本题思路:后序遍历,针对每一个子树始终左子树、右子树、根递归遍历。
代码:
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param root TreeNode类
* @return int整型ArrayList
*/
public ArrayList<Integer> postorderTraversal (TreeNode root) {
// write code here
ArrayList<Integer> list = new ArrayList<Integer>();
if (root == null) {
return list;
}
ArrayList<Integer> leftList = new ArrayList<Integer>();
ArrayList<Integer> rightList = new ArrayList<Integer>();
leftList = postorderTraversal(root.left);
rightList = postorderTraversal(root.right);
list.addAll(leftList);
list.addAll(rightList);
list.add(root.val);
return list;
}
}
题目CC7:(知识点:栈,树,难度:简单)
求给定的二叉树的前序遍历。
例如:
给定的二叉树为{1,#,2,3},
返回:[1,2,3].
代码:
import java.util.*;
/*
* public class TreeNode {
* int val = 0;
* TreeNode left = null;
* TreeNode right = null;
* }
*/
public class Solution {
/**
*
* @param root TreeNode类
* @return int整型ArrayList
*/
public ArrayList<Integer> preorderTraversal (TreeNode root) {
// write code here
ArrayList<Integer> list = new ArrayList<Integer>();
if (root == null) {
return list;
}
list.add(root.val);
ArrayList<Integer> leftList = new ArrayList<Integer>();
ArrayList<Integer> rightList = new ArrayList<Integer>();
leftList = preorderTraversal(root.left);
rightList = preorderTraversal(root.right);
list.addAll(leftList);
list.addAll(rightList);
return list;
}
}
题目CC8:重排链表(知识点:链表,难度:中等)
将给定的单链表L:L0–>L1–>…–>Ln-1–>Ln
重新排序为:L0–Ln–>L1–>Ln-1–>L2–>Ln-2->…
要求使用原地算法,不能只改变节点内部的值,需要对实际的节点进行交换。
数据范围:链表长度 0≤n≤20000 ,链表中每个节点的值满足 0≤val≤1000
要求:空间复杂度 O(n) 并在链表上进行操作而不新建链表,时间复杂度 O(n)
进阶:空间复杂度 O(1) , 时间复杂度 O(n)
示例1
输入:
{1,2,3,4}
返回值:
{1,4,2,3}
说明:
给定head链表1->2->3->4, 重新排列为 1->4->2->3,会取head链表里面的值打印输出 1
示例2
输入:
{1,2,3,4,5}
返回值:
{1,5,2,4,3}
说明:
给定head链表1->2->3->4->5, 重新排列为 1->5>2->4->3,会取head链表里面的值打印输出
示例3
输入:
{}
返回值:
{}
本题思路一:时间复杂度O(N):N是链表的节点数量,遍历链表,重组链表,空间复杂度O(1):使用常数级空间指针。
1.使用快慢指针,快指针一次走两步,慢指针一次走一步,当快指针走到终点时,慢指针指向中点,若节点个数为偶数,则慢指针指向中间两个节点中的左端节点。
2.通过第一步拆分两个链表,对后一半链表逆序,用头插法
3.遍历逆序后的链表插入到第一个链表的对应位置。
代码:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return;
}
// 用快慢指针寻找中间节点,将链表分为两个
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode newHead = slow.next;
slow.next = null;
// 第二个链表反转,这里考察单链表反转
newHead = reverseList(newHead);
// 链表节点依次连接
while(newHead != null) {
ListNode temp = newHead.next;
newHead.next = head.next;
head.next = newHead;
head = newHead.next;
newHead = temp;
}
}
private ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode tail = head;
head = head.next;
tail.next = null;
while (head != null) {
ListNode temp = head.next;
head.next = tail;
tail = head;
head = temp;
}
return tail;
}
}
总结1:考察快慢指针,链表逆序(头插法),单链表插入操作等。
本题思路二:将链表放到一个ArrayList中,利用前后双指针,依次按题目要求插入链表。时间复杂度O(N),空间复杂度O(N)
代码:
import java.util.*;
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return;
}
// 用快慢指针寻找中间节点,将链表分为两个
List<ListNode> list = new ArrayList<>();
while (head != null) {
list.add(head);
head = head.next;
}
// 设置两个指针
int i = 0; //前指针
int j = list.size() - 1; //后指针,增加索引好寻找最后一个元素,以便从后往前遍历
while (i < j) {
list.get(i).next = list.get(j);
i++;
if (i == j) { // 链表节点只有两个时才会出现这种情况
break;
}
list.get(j).next = list.get(i);
j--;
}
list.get(i).next = null;
}
}
总结2:这里关键是给单链表的每个元素增加了下标索引,使用ArrayList增加了O(N)的空间。
题目CC9:(知识点:链表,哈希,双指针,难度:中等)
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。
数据范围: 0<=n<=10000,1<=结点值<=10000
要求:空间复杂度 O(1),时间复杂度 O(n)
例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。
输入描述:
输入分为2段,第一段是入环前的链表部分,第二段是链表环的部分,后台会根据第二段是否为空将这两段组装成一个无环或者有环单链表
返回值描述:
返回链表的环的入口结点即可,我们后台程序会打印这个结点对应的结点值;若没有,则返回对应编程语言的空结点即可。
示例1
输入:
{1,2},{3,4,5}
返回值:
3
说明:
返回环形链表入口结点,我们后台程序会打印该环形链表入口结点对应的结点值,即3
示例2
输入:
{1},{}
返回值:
“null”
说明:
没有环,返回对应编程语言的空结点,后台程序会打印"null"
示例3
输入:
{},{2}
复制
返回值:
2
说明:
环的部分只有一个结点,所以返回该环形链表入口结点,后台程序打印该结点对应的结点值,即2
本题思路一:hash法,使用HashSet存储已经遍历过的节点,当第一次出现重复的节点时,即为入口节点。时间复杂度:O(N),需要将链表的所有节点遍历一次;空间复杂度:需要额外的O(N)空间HashSet存储节点。
代码:
import java.util.*;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
HashSet<ListNode> set = new HashSet<>();
if (pHead == null) {
return null;
}
while (pHead != null) {
// 当set中包含节点时,说明第一次出现重复的节点,即环的入口节点。
if (set.contains(pHead)) {
return pHead;
}
// set中加入未重复的节点
set.add(pHead);
pHead = pHead.next;
}
return null;
}
}
本题思路二:快慢指针很容易判断是否有环,进入环中,二者间的相对距离每次减1,最终会相遇。假设从头节点到环的入口节点的前一个节点一共有a个,环中的节点有b个,设fast指针走过的节点数为f,slow指针走过的节点数为s,则:
(1) f=2s
(2) f=s+nb (f走的节点数一定与s走的节点数相差整数圈即nb)
由此得到s=nb.两指针相遇时满指针走了nb步。已知指针走到环的入口节点需要走a+kb步(k为整数),而slow指针走了nb步,则其只要走a步即可到环的入口节点,这时将fast指针赋值为头节点,也移动a步后则fast和slow指针在环的入口节点相遇(编程的具体实现思想)。
时间复杂度:O(n),n为链表节点数,以满指针为基准,遍历次数约为2次
空间复杂度:O(1),只使用了两个指针变量,空间复杂度为常数。
代码:
import java.util.*;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead == null) return null;
// 定义快慢指针
ListNode slow = pHead;
ListNode fast = pHead;
while (fast != null && fast.next != null) {
// 快指针是慢指针的两倍速度
fast = fast.next.next;
slow = slow.next;
// 记录快慢指针第一次相遇的节点
if (slow == fast) break;
}
// 若快指针指向null,则环不存在
if (fast == null || fast.next == null) return null;
// 将快指针重新指向头节点
fast = pHead;
// 与第一次相遇的节点相同速度出发 这里是这道题的核心点
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
题目CC10 判断链表中是否有环
知识点:链表、哈希、双指针(快慢指针)
描述
判断给定的链表中是否有环。如果有环则返回true,否则返回false。
数据范围:链表长度 0 \le n \le 100000≤n≤10000,链表中任意节点的值满足 |val| <= 100000∣val∣<=100000
要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
输入分为两部分,第一部分为链表,第二部分代表是否有环,然后将组成的head头结点传入到函数里面。-1代表无环,其它的数字代表有环,这些参数解释仅仅是为了方便读者自测调试。实际在编程时读入的是链表的头节点。
例如输入{3,2,0,-4},1时,对应的链表结构如下图所示:
可以看出环的入口结点为从头结点开始的第1个结点(注:头结点为第0个结点),所以输出true。
示例1
输入:{3,2,0,-4},1
返回值:true
说明:第一部分{3,2,0,-4}代表一个链表,第二部分的1表示,-4到位置1(注:头结点为位置0),即-4->2存在一个链接,组成传入的head为一个带环的链表,返回true
示例2
输入:{1},-1
返回值:false
说明:第一部分{1}代表一个链表,-1代表无环,组成传入head为一个无环的单链表,返回false
示例3
输入:{-1,-7,7,-4,19,6,-9,-5,-2,-5},6
返回值:true
此题与CC9题相关为其一部分,解题简单。
代码如下:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
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) break;
}
// 无环
if (fast == null || fast.next == null) return false;
// 有环
return true;
}
}