算法题:就是确定需要使用哪种遍历算法、哪个数据结构来解题。
简单算法
03、数组中重复的数字(利用数组索引和值一对一的特性)
【一个长度为n的数组nums、里面的所有数字范围在 0~n-1内】
但有些数字是重复的。却不知道哪些重复、请找出任意一个重复数字
解法一:利用Set集合的唯一性(时间复杂度O(n) 空间复杂度O(n))
public int findRepeatNumber(int[] nums) {
Set<Integer> dic = new HashSet<>();
for(int num : nums) {
if(dic.contains(num)) return num;
dic.add(num);
}
return -1;
}
解法二:原地交换(需要数组中值的范围0~n-1在数组长度n内)(时间复杂度O(n) 空间复杂度O(1))
该题数组中的下标和值不是一一对应的。而是一对多的关系。
因此可以将值放在该值对应的索引处、如果交换时该位置已经有值、则代表重复了
利用索引和值的一一对应性
private static int findRepeatNumber(int[] nums){
int i = 0, n = nums.length;
while (i < n){
// 如果该值对应的索引就是他自己
if (nums[i] == i){
i++;
continue;
}
int temp = nums[i];
// 如果该值对应的索引处已经调整过值
if(nums[temp] == temp){
return nums[i];
}
// 否则、交换值
nums[i] = nums[temp];
nums[temp] = temp;
}
return -1;
}
05、替换空格【String类不可变】
将给定字符串中的空格、替换为%20、
private String replaceBlank(String s){
// Java中String类不可变、如果在String上替换、每次都会新建一个String对象。
// 因此使用StringBuilder 来操作
StringBuilder res = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == ' '){
res.append("%20");
}else {
res.append(c);
}
}
return res.toString();
}
06、从尾到头打印链表【递归、栈】
给你一个单向链表、用数组按反着的顺序输出链表的值
private static List<Integer> res = new ArrayList<>();
private static int[] printReverseListNode(ListNode head){
print(head);
int[] result = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
result[i] = res.get(i);
}
return result;
}
private static void print(ListNode head){
// 递归到链表末尾(递归出口):返回
if (head == null){
return;
}
// 递归
print(head.next);
// 递归到末尾返回后、走到这一步、add末尾结点的值
res.add(head.val);
}
08、双栈实现队列
用两个栈实现队列的尾插和头删两个方法
class StackImplQueue{
private Stack<Integer> stack1;
private Stack<Integer> stack2;
public StackImplQueue(){
this.stack1 = new Stack<>();
this.stack2 = new Stack<>();
}
public void appendTail(int e){
stack1.push(e);
}
public int deleteHead(){
while (stack1 != null && stack1.size() > 0){
stack2.push(stack1.pop());
}
Integer tem = stack2.pop();
System.out.println("头元素:" + tem + ",删除成功!");
return tem;
}
public void outPut(){
while (stack1 != null && stack1.size() > 0){
stack2.push(stack1.pop());
}
StringBuilder sb = new StringBuilder();
int[] temp = new int[stack2.size()];
int i = 0;
while (stack2 != null && stack2.size() > 0){
Integer num = stack2.pop();
sb.append(num);
temp[i++] = num;
}
System.out.println(sb);
for (int e : temp) {
stack1.push(e);
}
}
}
10、斐波那契数列(青蛙跳台阶)
写一个函数、输入n,求斐波那契数列的第n项
斐波那契数列的定义:F(0) = 0,F(1) = 1,F(N) = F(N - 1) + F(N - 2)
为保证结果不超过整型上限、要将结果对1000000007取模
解法一(递归)
private static int getFibonacci(int n){
if (n < 3){
return n;
}
return (getFibonacci(n - 1) + getFibonacci(n - 2)) % 1000000007;
}
解法二(动态规划)
private static int getFibonacci(int n){
int a = 0, b = 1, sum = 0;
for (int i = 0; i < n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return sum;
}
11、旋转数组的最小值
把一个数组最开始的若干个元素放到数组末尾、称为数组的旋转。
现在:输入一个递增排序的数组的旋转、请输出其旋转数组的最小值。
即使是旋转数组、因为原数组是递增的、也可以用二分查找来提升效率
接下来就是判断:每次二分该往左还是往右查找
几种情况:2,2,2,0,1、5,6,0,1,2,3,4、1,2,0,0,0、2,3,0,1,1
private static int minNumInSpinArray(int[] nums){
int len = nums.length;
if (len == 0){
return -1;
}
if (len == 1){
return nums[0];
}
int left = 0, right = len - 1, min = Integer.MAX_VALUE;
while (left < right){
int mid = (left + right) / 2;
//根据枚举的几种情况、可以得出以下判断条件
if (nums[mid] >= nums[right]){
left = mid + 1;
}else {
right = mid - 1;
}
min = Math.min(min, nums[mid]);
}
return min;
}
15、汉明重量(计算二进制数中1的个数)
n & ( n - 1 ) <==> 将二进制数n最低一位的0 变为1
public int hammingWeight(int n) {
int ret = 0;
while (n != 0) {
n &= n - 1;
ret++;
}
return ret;
}
21、调整数组顺序【快慢指针】
将数组顺序调整成: 前面全是奇数、后面全是偶数。奇数/偶数部分不用排序
private static int[] printNumUntilN(int[] nums) {
int slow = 0, fast = 0;
int len = nums.length;
while (slow < len) {
// 如果slow是偶数
if (nums[slow] % 2 == 0) {
while (fast < len) {
// 如果找到一个奇数、就换位
if (nums[fast] % 2 == 1) {
int temp = nums[fast];
nums[fast] = nums[slow];
nums[slow] = temp;
// 如果slow后面全是偶数了、则已经满足题目要求、设置flag
fast = -1;
break;
}
fast++;
}
// 已经满足题目要求、提前结束循环。
// 否则slow指针要一直遍历到末尾才会结束。
if (fast != -1) {
return nums;
}
}
slow++;
// fast指针归位
fast = slow;
}
return nums;
}
22、链表倒数第K个节点
双指针L R、先移动R、直到L是R的倒数第K个时、LR一起移动。R.NEXT = NULL时返回
private static ListNode findKDesc(ListNode head, int k){
ListNode left = new ListNode(0);
left.next = head;
ListNode right = left;
while (right.next != null){
right = right.next;
if (k > 1){
k--;
}else {
left = left.next;
}
}
if (k > 1){
return null;
}
return left;
}
24、反转链表
方法一:非递归
private static ListNode reverse(ListNode head) {
ListNode tail = head;
while (tail != null) {
tail = tail.next;
}
// 前一节点
ListNode newHead = null;
ListNode next = null;
while (head != tail) {
// 先用temp变量暂存head.next节点(摘下 head的next指针)
// 拿到头结点的下一个节点:next节点
next = head.next;
// 翻转:让head的指针指向newHead (第一轮时newHead=null)
head.next = newHead;
// 将本轮head赋给newHead(本轮的head就是新的newHead)
newHead = head;
// 将head指针后移一位(head = head.next 当前next设为新的head)
head = next;
}
return newHead;
}
方法二:递归
public static ListNode reverseList(ListNode head) {
return recur(head, null); // 调用递归并返回
}
private static ListNode recur(ListNode cur, ListNode pre) {
if (cur == null) { // 终止条件
return pre; // pre为反转后的头结点、一直return回第一层
}
ListNode res = recur(cur.next, cur); // 递归后继节点
cur.next = pre; // 反转指针
return res; // 返回反转链表的头节点
}
25、合并两个有序链表
private static ListNode merge(ListNode head1, ListNode head2) {
// 确保head1的头结点 < head2; 即head1的头结点为合并后的链表头
if (head1.val > head2.val) {
return merge(head2, head1);
}
ListNode pointer = head1;
// 对链表1进行遍历、逐个与链表2比较。
while (pointer.next != null) {
// 如果此时head2==null、说明head2更短、已经全部合并到head1了、则合并结束、直接返回!
if (head2 == null) {
return head1;
}
// 小、则继续往下比
if (pointer.next.val <= head2.val) {
pointer = pointer.next;
} else {
// 大、则①将head2.next摘下(暂存) ②将head2接到head1中 ③将pointer指向head2
ListNode temp = head2.next;
head2.next = pointer.next;
pointer.next = head2;
head2 = temp; // 重置head2
}
}
// 全部结束后、将head2剩余的元素直接接到head1尾部
head2 != null ? pointer.next = head2 : pointer.next = null;
return head1;
}
26、树的子结构
判定树B是否是树A的子树
public static void main(String[] args) {
TreeNode rootA = new TreeNode(3);
TreeNode a1 = new TreeNode(4);
TreeNode a2 = new TreeNode(5);
TreeNode a3 = new TreeNode(1);
TreeNode a4 = new TreeNode(2);
rootA.left = a1;
rootA.right = a2;
a1.left = a3;
a1.right = a4;
TreeNode rootB = new TreeNode(3);
TreeNode b1 = new TreeNode(4);
TreeNode b2 = new TreeNode(2);
rootB.left = b1;
b1.right = b2;
System.out.println(isSonTree(rootA, rootB));
}
private static boolean isSonTree(TreeNode A, TreeNode B) {
if (B == null) {
return false;
}
// 先序遍历树A
LinkedList<TreeNode> list = new LinkedList<>();
list.add(A);
while (list != null && list.size() > 0) {
TreeNode treeNode = list.removeFirst();
if (treeNode.left != null) {
list.add(treeNode.left);
}
if (treeNode.right != null) {
list.add(treeNode.right);
}
// 当树A的某个节点treeNode==B的根节点时。比较以treeNode为根的子树是否包含树B
if (treeNode.value == B.value) {
// 比较是否是子树
return compare(treeNode, B);
}
}
return false;
}
private static boolean compare(TreeNode A, TreeNode B) {
if (B == null) {
return true;
}
if (B != null && A != null && A.value == B.value) {
// 左子树都相同、并且右子树都相同时,才符合判断
return compare(A.left, B.left) && compare(A.right, B.right);
}
return false;
}
28、判断二叉树是否对称
public static void main(String[] args) {
TreeNode rootA = new TreeNode(4);
TreeNode a1 = new TreeNode(2);
TreeNode a2 = new TreeNode(2);
TreeNode a3 = new TreeNode(3);
TreeNode a4 = new TreeNode(4);
TreeNode a5 = new TreeNode(4);
TreeNode a6 = new TreeNode(3);
rootA.left = a1;
rootA.right = a2;
a1.left = a3;
a1.right = a4;
a2.left = a5;
a2.right = a6;
System.out.println(mirror(rootA));
}
private static boolean mirror(TreeNode root) {
if (root == null) {
return true;
}
return compare(root.left, root.right);
}
private static boolean compare(TreeNode A, TreeNode B) {
if (A == null && B == null) {
return true;
}
if (A == null || B == null) {
return false;
}
return A.value == B.value ? compare(A.left, B.right) && compare(A.right, B.left) : false;
}
29、顺时针打印矩阵
public static void main(String[] args) {
int[][] matrix = {new int[]{1, 2, 3, 4}, new int[]{5, 6, 7, 8}, new int[]{9, 10, 11, 12}};
printClockwiseMatrix(matrix);
}
private static void printClockwiseMatrix(int[][] matrix) {
boolean[][] walked = new boolean[matrix[0].length + 1][matrix.length + 1];
StringBuilder sb = new StringBuilder();
int i = 0, j = 0;
sb.append(matrix[0][0]).append(" ");
walked[0][0] = true;
while (true) {
int now = -1;
if (j + 1 < matrix[0].length && !walked[i][j + 1]) {
now = matrix[i][++j];
} else if (i + 1 < matrix.length && !walked[i + 1][j]) {
now = matrix[++i][j];
} else if (j - 1 >= 0 && !walked[i][j - 1]) {
now = matrix[i][--j];
} else if (i - 1 >= 0 && !walked[i - 1][j]) {
now = matrix[--i][j];
}
if (now != -1) {
walked[i][j] = true;
sb.append(now).append(" ");
} else {
break;
}
}
System.out.println(sb);
}
==============================================================================
中级算法
04、二维数组的查找
给定一个二维数组和target目标值、如果二维数组中有target则返回true
该二维数组:同一行的数据向右 / 同一列的数据向下 是递增的!
将矩阵逆时针旋转45°、则可以看成一个二叉排序树:左小右大。右上角/左下角元素等价于根结点
private static boolean findTargetInMatrix(int[][] matrix, int target){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return false;
}
int row = matrix.length, columns = matrix[0].length;
int i = row,j = 0;
//从【左下角元素开始排查】、比该元素小的都在其上方、比他大的都在右边。
// (从右上角元素排查也可以、因为他们)
while(i >= 0 && j < columns)
{
if(matrix[i][j] > target){
i--;
} else if(matrix[i][j] < target){
j++;
} else{
return true;
}
}
return false;
}
07、重构二叉树【分治算法】
给你一个二叉树的【先序遍历、中序遍历】结果。请你重构出二叉树
先序遍历的特点:第一个值就是二叉树的根结点
按照:根节点、左子树根节点、左子树根节点的左子树根节点 、… … 、再右子树根节点…的顺序
中序遍历的特点:给定一个根节点、在中序遍历的结果中。该节点的左边一系列节点构成左子树、
右边一系列节点构成其右子树
因此:我们可以根据先序遍历第一个值(根节点)在中序遍历结果中的索引、分出其左右子树、然后递归下去
private static int[] preorder;
private static HashMap<Integer, Integer> dic = new HashMap<>();
private static TreeNode rebuildBinaryTree(int[] preOrder, int[] inOrder){
preorder = preOrder;
if (preOrder.length == 1 && inOrder.length == 1){
return new TreeNode(preOrder[0]);
}
// 为了提升效率(快速寻找根节点在中序遍历里的位置)、
// 用HashMap(val,index)记录中序遍历的结果
for(int i = 0; i < inOrder.length; i++){
dic.put(inOrder[i], i);
}
return recur(0,0,inOrder.length - 1);
}
// 【root:根节点在先序遍历中的索引 left/right:左/右结点在中序遍历里的索引
// left~right标记了当前子树的结点、在中序遍历数组中的索引范围】
private static TreeNode recur(int root, int left, int right) {
// 如果中序遍历中的 左索引 > 右索引:递归终止
if(left > right){
return null;
}
// new出当前根节点
TreeNode node = new TreeNode(preorder[root]);
// 找到当前根节点在中序遍历中的下标,根据下标一分为二:左边属于左子树、右边属于右子树
int rootIndex = dic.get(preorder[root]);
// 当前根节点的左指针、指向左子树的根节点
// root+1:根据先序遍历的特点、下一个值(root+1)就是当前根节点的左节点(左子树的根节点)
// 左子树结点在中序遍历数组里的范围为left ~ rootIndex-1
// (rootIndex - 1:当前根节点在中序遍历中的索引再往左移一位)
node.left = recur(root + 1, left, rootIndex - 1);
// 当前根节点的右指针、指向右子树的根节点
// 右子树的根节点在先序遍历数组里的索引:root + rootIndex - left + 1
// 即:根节点索引 + 左子树长度 + 1)
// 右子树结点的范围为rootIndex + 1 ~ right
// (rootIndex + 1:当前根节点在中序遍历中的索引再往右移一位)
node.right = recur(root + rootIndex - left + 1, rootIndex + 1, right);
// 返回当前子树的根节点。
return node;
}
public static void main(String[] args) {
int[] preOrder = {3,9,20,15,7};
int[] inOrder = {9,3,15,20,7};
TreeNode root = rebuildBinaryTree(preOrder, inOrder);
bfs(root);
dfs(root);
}
// 广度优先遍历
public static void bfs(TreeNode root){
LinkedList<TreeNode> list = new LinkedList<>();
list.add(root);
while (list != null && list.size() > 0){
TreeNode node = list.removeFirst();
System.out.println(node.val);
if (node.left != null){
list.add(node.left);
}
if (node.right != null){
list.add(node.right);
}
}
}
// 先序遍历
public static void dfs(TreeNode root){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (stack != null && stack.size() > 0){
TreeNode node = stack.pop();
System.out.println(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
}
static class TreeNode{
private int val;
private TreeNode left;
private TreeNode right;
public TreeNode(int value){
this.val = value;
}
}
12、矩阵中的路径【回溯+递归】
回溯+递归
这里涉及到4个方向的查找、巧妙之处在于:用 短路或 || 、来联合4个方向的查找
// 格子是否已经被走过
private static boolean[][] flag = new boolean[4][4];
public static void main(String[] args) {
char[][] chars = new char[4][4];
chars[0][0] = 'A';
chars[0][1] = 'B';
chars[0][2] = 'C';
chars[0][3] = 'E';
chars[1][0] = 'S';
chars[1][1] = 'F';
chars[1][2] = 'C';
chars[1][3] = 'S';
chars[2][0] = 'A';
chars[2][1] = 'D';
chars[2][2] = 'E';
chars[2][3] = 'E';
System.out.println(findWordInMatrix(chars, "ABCCED"));
}
private static boolean findWordInMatrix(char[][] board, String word) {
// 先假设一个起点、然后再递归找邻格
for (int j = 0; j < board.length; j++) {
for (int i = 0; i < board[0].length; i++) {
if (find(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
private static boolean find(char[][] board, String word, int i, int j, int index) {
// 本轮i j不合法、或格子走过了、或字符不匹配、返回false(回溯)
if (i < 0 || i >= board[0].length || j < 0 || j >= board.length || flag[i][j] || board[i][j] != word.charAt(index)) {
return false;
}
// ——> 否则:说明 本次格子符合条件
// 设置该格为已走过
flag[i][j] = true;
// (方法出口):本轮index等于word.length-1:即全部字符都找完了
if (index == word.length() - 1) {
return true;
}
// 巧妙的应用短路或|| 来进行四个方向的查找
// 短路或的特性:四个方向只要有一个方向返回true、说明这条路走得通
// 如果四个方向全为 false、说明走到死胡同了(该路径不对)。则回溯到上一格。
boolean res = find(board, word, i + 1, j, index + 1) || find(board, word, i - 1, j, index + 1) ||
find(board, word, i, j + 1, index + 1) || find(board, word, i, j - 1, index + 1);
// 如果回溯回来了:就将本轮走的格子重置为 未走过
flag[i][j] = false;
return res;
}
13、机器人的运动路径
我的解法:时间复杂度最多也就是O(mn)、
private static int robotPath(int m, int n, int k){
int result = 0;
outer : for (int i = 0; i < m; i++) {
if ((i % 10 + i / 10) > k){
break;
}
for (int j = 0; j < n; j++) {
int now = i % 10 + i / 10 + j % 10 + j / 10;
if (now > k){
continue outer;
}
result++;
}
}
return result;
}
力扣解法(类似于12题 回溯+递归):
private static int m,n,k;
private static boolean[][] visited;
private static int robotPath(int x, int y, int z){
m = x;
n = y;
k = z;
visited = new boolean[x][y];
return dfs(0,0) + 1;
}
private static int dfs(int i,int j){
// 如果这次不满足条件(出口):回溯:返回0(这个方格访问不到)
if (i >= m || j >= n || (i % 10 + i / 10 + j % 10 + j / 10) >= k || visited[i][j]){
return 0;
}
// 否则:访问的到:设为已访问过
visited[i][j] = true;
// 递归:查看向右或向下的邻格是否符合条件
return 1 + dfs(i+1, j) + 1 + dfs(i, j+1);
}
}
14、切绳子 I (数学推导)
将一根长为n的绳子、切成长度为m的小段、如何切?使得切分出的各段的长度乘积最大?
eg:n = 8; m = 3; 切出(8 = 3+3+2):3 * 3 * 2 = 18
private static int cut(int n){
// 根据【数学推导】:切出的绳子长度为3或最接近3时、乘积最大
if (n <= 3){
return n-1;
}
// a:分为几个段 b: 分完后余下几
int a = n / 3, b = n % 3;
if (b == 0){
return (int) Math.pow(3, a);
}
if (b == 1){
// 将一个 1+3 转为 2 + 2
return (int) Math.pow(3, a - 1) * 2 * 2;
}
// 直接再乘以余出来的2
return (int) Math.pow(3, a) * 2;
}
31、栈的压入、弹出顺序
以指定的顺序压入栈后,判定该弹出顺序是否合法。
public boolean validStackSequences(int[] pushed, int[] popped) {
// 使用一个栈来模拟这个压入弹出顺序,如果最后栈为空,说明弹出顺序合法(能够顺利弹出)
Stack<Integer> stack = new Stack<>();
int i = 0;
for (int item : pushed) {
stack.push(item);
while (!stack.isEmpty() && stack.peek().equals(popped[i])) {
stack.pop();
i++;
}
}
return stack.isEmpty();
}
34、二叉树中和为某值的路径
private List<LinkedList<Integer>> findSumTarget(TreeNode head, int target) {
this.target = target;
find(head);
return result;
}
private void find(TreeNode head) {
if (head == null || list.stream().reduce(Integer::sum).orElse(0) + head.value > target) {
return;
}
list.add(head.value);
if (list.stream().reduce(Integer::sum).orElse(0) == target && head.left == null && head.right == null) {
result.add(new LinkedList(list));
}
find(head.left);
find(head.right);
list.removeLast();
}
36、二叉搜索树改造为循环链表
// 定义双向链表的头结点head。 每次递归的前驱节点pre
static TreeNode pre, head;
public static TreeNode treeToDoublyList(TreeNode root) {
if (root == null) return null;
dfs(root);
// 双向链表形成后。将头尾节点的指针互相连起来,形成循环
head.left = pre;
pre.right = head;
return head;
}
static void dfs(TreeNode cur) {
// 二叉搜索树中序遍历的结果就是递增的。因此根据中序递归遍历来改造
if (cur == null) {
return;
}
// 递归左子树
dfs(cur.left);
//-----------业务逻辑处理(原生中序遍历是输出节点值sout(root.value) / 但这里是需要修改指针朝向)---------
if (pre != null) {
// pre!=null说明本次递归非头结点,则需要修改双向指针pre.right cur.left
pre.right = cur;
} else {
// pre==null,代表正在访问链表头的结点。将该节点记为head。将head.left指向pre即可
head = cur;
}
cur.left = pre;
pre = cur;
//--------------------
// 递归左子树
dfs(cur.right);
}