0 前言

邻近校招,算法要命!!!

本文为研究剑指Offer过程中的笔记,整理出主要思路以及Java版本题解,以便记忆和复习。

参考整理来自《剑指Offer 第二版》。

特别注意,对每道题要首先考虑解题之外的要点:

特殊输入:传参为null,数组长度为0,空字符串等。

边界条件:数组、字符串长度是否满足题目要求等。

数值处理:对于数值考虑是否需要正负号,是否溢出等问题。

1 实现Singleton模式

题目

设计一个类,我们只能生成该类的一个实例。

思路

实现单例模式的基本步骤如下:

构造函数私有化,保证不能重复创建新实例;

提供一个类属性,用来保存实例化后的该单例;

提供一个getInstance()静态方法,该方法用来向外提供该单例。

实现单例模式有很多种方法,包括不加锁、方法锁、双重锁、静态内部类、枚举等。其中:

不加锁只适用于单线程环境。

方法锁适用于多线程环境但是性能较差。

双重锁优化了性能问题,但由于可能发生指令重排,因此需要对单例类属性添加volatile关键子。

静态内部类和枚举既适用于多线程环境,性能也较好。

为了解决反序列化问题,可以添加readResolve()方法。

枚举类天然防止反射攻击、反序列化问题。其他方法均可能出现这些问题。

代码

在这里只列出静态内部类的实现方式,其他方式可以参考https://blog.nowcoder.net/n/df5b458b6f1b47e490b60051d5e3dc13。

这种方法利用了JVM类加载机制,是懒汉式单例模式:

在第一次调用getInstance()方法前,没有直接使用过Inner类,因此它没有被初始化,instance为默认值null。

当调用getInstance()方法后,保证了线程安全,instance即被赋值为单例。

由于instance被保存在Inner类属性中,之后每次调用getInstance()获取到的都是同一个实例对象。

public class Singleton {

// 1、构造函数私有化

private Singleton() {

}

// 2、提供一个类属性用于保存单例,这里用一个内部类保存

private static class Inner {

private static Singleton instance = new Singleton();

}

// 3、提供一个静态的获取该实例的方法

public static Singleton getInstance() {

return Inner.instance;

}

}

2 数组中重复的数字

2.1 题目一:找出数组中重复的数字

题目

在一个长度为n的数组里的所有数字都在0~(n-1)的范围内。数组中的某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字2或者3。

思路

三种方案:

先将数组排序,遍历数组找出重复数字(快速排序的时间复杂度为O(nlogn))。

遍历数组过程中,使用哈希表或数组记录,如果已存在该数字,则找到(时间复杂度为O(n),空间复杂度为O(n))。

遍历数组,根据索引与值的关系,将该索引对应的值从其他地方交换回来,如果发现待交换的两个值相同,则找到重复数字(时间复杂度O(n))。

代码

前两种实现起来都比较简单,而第三种效率最高,因此这里主要讲解第三种实现方法。

实现步骤如下:

需要按照索引依次遍历,将每个索引位置上的值找回来,因此外层是一个for (int i = 0; i < length; i++)循环。

对于每个索引上的值,有两种情况:

index == value,说明该索引位置上的值已经归位;

index != value,说明该索引位置上的值没有归位,需要循环将当前值(arr[index])与arr[value]进行交换(可以理解成将value归位),直到当前index == value,即将当前索引位置上的值归位。

在交换的过程中,如果发现待交换的索引位置上的值发生了碰撞,两个值重复了,即找到了重复数字。

如果对索引遍历结束后,都没有发生交换碰撞,则说明此数组中没有重复数字。

以题目中长度为7的数组{2, 3, 1, 0, 2, 5, 3}为例:

依次遍历索引0, 1, 2, 3, 4, 5, 6,对该索引上的值进行判断。

以索引0上的值2为例,发现0 != 2,因此需要对索引0上的值进行归位。

首先将arr[0]与arr[2]进行交换,将值2进行归位,得到数组{1, 3, 2, 0, 2, 5, 3}。

此时发现索引0上的值为1,0 != 1,因此继续归位,将值1进行归位,得到数组{3, 1, 2, 0, 2, 5, 3}。

此时发现索引0上的值为3,0 != 3,继续归位,将值3进行归位,得到数组{0, 1, 2, 3, 2, 5, 3}。

此时发现索引0上的值为0,0 == 0,即索引0上的值归位成功。

按照上述方法,遍历索引1, 2, 3, 4, 5, 6,对该索引上的值进行归位。

当遍历到索引4时,发现值2,与索引2上的值发生了碰撞,即找到了重复数字2。

import java.util.ArrayList;

import java.util.List;

public class GetOffer {

/**

* 找出数组中重复的数字

* @param arr 数组

* @param duplication 重复的数字

* @return 是否有重复的数字

*/

public static boolean duplicate(int[] arr, List duplication) {

// 1、处理特殊输入等问题

if (arr == null || arr.length <= 0) {

return false; // 数组为空,或者数组长度不满足题目要求

}

if (duplication == null) {

throw new RuntimeException("请输入存放重复数字的容器"); // duplication为null

}

// 2、解题

// 对索引进行遍历,对每个索引进行归位

int length = arr.length;

for (int i = 0; i < length; i++) {

if (arr[i] < 0 || arr[i] > length - 1) {

return false; // 数组中的值不满足题目要求

}

// 如果 i != arr[i],则需要进行归位

while (i != arr[i]) {

// 发生碰撞,找到重复数字,保存并返回true

if (arr[i] == arr[arr[i]]) {

duplication.add(arr[i]);

return true;

}

// 交换,对当前值进行归位

swap(arr, i, arr[i]);

}

// 归位成功后,继续遍历下一个索引

}

// 遍历结束后都没有发生碰撞,说明数组中没有重复数字

return false;

}

private static void swap(int[] arr, int i, int j) {

int temp = arr[i];

arr[i] = arr[j];

arr[j] = temp;

}

/**

* 测试

* @param args

*/

public static void main(String[] args) {

int[] arr = {2, 3, 1, 0, 2, 5, 3};

List duplication = new ArrayList<>();

boolean isDup = duplicate(arr, duplication);

if (isDup) {

System.out.println("发现重复数字:" + duplication.get(0));

} else {

System.out.println("没有发现重复数字");

}

}

}

2.2 不修改数组找出重复的数字

题目

在一个长度为n+1的数组里的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。

请找出数组中任意一个重复的数字,但不能修改输入的数组。

例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字2或者3。

思路

根据题意,数组中的值的范围为[1, n],即重复的值必定在这个范围中。

我们把从1n的数字从中间的数字m分为两部分,前面一半为1m,后面一半为m+1~n。

如果1m的数字的数目超过(m+1)-1,那么这一半的区间里一定包含重复的数字;否则,另一半m+1n的区间里一定包含重复的数字。由此缩小了范围。

重复将范围一分为二,并进行判断,最终即可确定重复的值。

以长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7}为例,值的范围为[1, 8]。

我们将值的范围分为[1, 4]和[5, 8]两部分。遍历数组,发现值在前半部分(即[1, 4])的数量为5,超过了4(4+1-1),说明重复的值的范围是[1, 4]。

继续缩小范围,分为[1, 2]和[3, 4]两部分。遍历数组,发现值在前半部分(即[1, 2])的数量为2,不大于2(2+1-1),说明后半部分(即[3, 4])中必定存在重复数字。【注意:并不能说明前半部分中没有重复的数字】

缩小范围,分为3、4两部分。遍历数组,发现值在前半部分(即值等于3)的数量为2,大于1(3+1-3),说明重复的值的范围是3。并且此时发现范围开始索引(3)等于范围结束索引(3),说明重复值为3。

代码

import java.util.ArrayList;

import java.util.List;

public class GetOffer {

public static int getDuplication(int[] arr) {

// 1、处理特殊输入等问题

if (arr == null) {

return -1;

}

int length = arr.length;

for (int i = 0; i < length; i++) {

if (arr[i] < 1 || arr[i] > length) {

return -1; // 数组中的值不满足题目要求

}

}

// 2、解题

int left = 1;

int right = length - 1;

while (left < right) {

// 将范围分成两部分:[left, middle]、[middle+1, right]

int middle = ((right - left) >> 1) + left;

// 统计数组在前半部分中的数量

int count = 0;

for (int i = 0; i < length; i++) {

if (arr[i] >= left && arr[i] <= middle) {

count++;

}

}

// 如果值在前半部分,则范围缩小为[left, middle],否则为[middle + 1, right]

if (count > (middle + 1 - left)) {

right = middle;

} else {

left = middle + 1;

}

}

// 最后left = right,从而锁定重复的值

return left;

}

/**

* 测试

*

* @param args

*/

public static void main(String[] args) {

int[] arr = {2, 3, 5, 4, 3, 2, 6, 7};

int duplication = getDuplication(arr);

System.out.println(duplication); // 3

}

}

3 二维数组中的查找

题目

在一个二维数组中,每一行都按照从左到右递增的顺序排列,每一列都按照从上到下递增的顺序排列。

请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路

例如有如下数组:

1 2 3 4 5

2 3 4 5 6

3 4 5 6 7

从右上角开始遍历:

如果该数字等于要查找的数字,则查找过程结束;

如果该数字大于要查找的数字,则剔除这个数字所在的列(col--);

如果该数字小于要查找的数字,则剔除这个数字所在的行(row++)。

每次都可以在数组的查找范围中剔除一行或者一列。

代码

public class Solution {

public boolean Find(int target, int [][] array) {

// 判断特殊输入

if (array == null) {

return false;

}

int rows = array.length;

if (rows == 0) {

return false;

}

int cols = array[0].length;

if (cols == 0) {

return false;

}

// 从右上角往左下角遍历

int row = 0; // 第一行

int col = cols - 1; // 第一列

while (row < rows && col >= 0) {

if (array[row][col] == target) { // 当前值为target

return true;

} else if (array[row][col] > target) { // 当前值大于target,说明在左侧

col--;

} else { // 当前值小于target,说明在下面

row++;

}

}

return false; // 遍历结束,没有找到

}

}

4 从尾到头打印链表

题目

输入一个链表的头节点,从尾到头反过来打印出每个节点的值。

思路

方法一:栈

从头到尾遍历链表,每经过一个节点的时候,把该节点放到一个栈中。当遍历完整个链表后,再从栈顶开始逐个输出节点的值,此时输出的节点的顺序已经反过来了。

方法二:递归

每访问到一个节点的时候,如果有下一个节点就先递归输出下一个节点,再输出本节点自身。

代码

方法一:栈

import java.util.*;

public class Solution {

private class ListNode {

int val;

ListNode next = null;

ListNode(int val) {

this.val = val;

}

}

public ArrayList printListFromTailToHead(ListNode listNode) {

ArrayList list = new ArrayList<>();

// 判断特殊输入

if (listNode == null) {

return list;

}

// 遍历链表,将值存入栈中

Stack stack = new Stack<>();

stack.push(listNode.val);

while (listNode.next != null) {

listNode = listNode.next;

stack.push(listNode.val);

}

// 遍历栈,将值存入集合

while (!stack.isEmpty()) {

list.add(stack.pop());

}

return list;

}

}

方法二:递归(超时)

import java.util.*;

public class Solution {

private class ListNode {

int val;

ListNode next = null;

ListNode(int val) {

this.val = val;

}

}

public ArrayList printListFromTailToHead(ListNode listNode) {

ArrayList list = new ArrayList<>();

// 判断特殊输入

if (listNode == null) {

return list;

}

while (listNode.next != null) {

// 添加下一个节点的值

list.addAll(printListFromTailToHead(listNode.next));

// 添加当前节点的值

list.add(listNode.val);

}

return list;

}

}

5 重建二叉树

题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,输入前序遍历序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6},则重建二叉树并返回它的头节点。

思路

将前序遍历序列和中序遍历序列看成一组。它们长度相等,代表当前二叉树。

首先,我们需要在当前前序遍历序列和中序遍历序列中找出当前子二叉树的根节点(最终返回的节点);

然后,分别确定左右子树的前序遍历序列和中序遍历序列,分别指向当前根节点的左子节点和右子节点;

如此递归下去,直到该子二叉树没有左右子树(即左右子树的前序遍历序列和中序遍历序列不可再分)。

因此,关键问题在于:如何确定左右子树的前序遍历序列和中序遍历序列?

对于前序遍历序列和中序遍历序列,我们可以发现:

在前序遍历序列中,第一个数字总是树的根节点的值;

在中序遍历序列中,根节点的值在序列的中部;

在中序遍历序列中,左子树的节点的值位于根节点的值的左边,右子树的节点的值位于根节点的值的右边;

在前序遍历序列中,左子树的节点的值位于根节点之后序列的前半部分,右子树的节点的值则位于后半部分(可以根据中序遍历得到左右子树的大小来确定)。

以前序遍历序列{1, 2, 3, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6}为例:

设置变量pL、pR、mL、mR分别代表前、中序遍历序列的开始索引和结束索引,最初分别为:0、7、0、7。

根据前序遍历序列可以得到根节点为:1(可以直接构建当前二叉树根节点);

由此可以查找根节点在中序遍历序列中的索引位置为:3;

在当前中序遍历序列中确定左子树的中序遍历序列为为{4, 7, 2},长度为3。进而在前序遍历序列中确定左子树的前序遍历序列为{2, 3, 7}。索引分别为:1、3、0、2。

按照同样的方法,确定右子树的前、中序遍历序列分别为{3, 5, 6, 8}和{5, 3, 8, 6}。索引分别为:4、7、4、7。

分别对左右子树进行递归,将返回的根节点连接在当前根节点上。

代码

/**

* Definition for binary tree

* public class TreeNode {

* int val;

* TreeNode left;

* TreeNode right;

* TreeNode(int x) { val = x; }

* }

*/

public class Solution {

public TreeNode reConstructBinaryTree(int [] pre,int [] in) {

// 特殊输入

if (pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length) {

return null;

}

return construct(pre, 0, pre.length - 1, in, 0, in.length - 1);

}

private TreeNode construct(int[] pre, int pL, int pR, int[] in, int iL, int iR) {

// 当前二叉树没有子树,直接返回当前根节点

if (pL > pR || iL > iR) {

return null;

}

// 1、根据前序遍历数列找出当前根节点的值

int val = pre[pL];

// 2、构建当前根节点

TreeNode root = new TreeNode(val);

// 3、根据根节点的值,找到其在中序遍历数列中的索引

int index = -1;

for (int i = iL; i <= iR; i++) {

if (val == in[i]) {

index = i;

}

}

if (index == -1) {

throw new RuntimeException("输入有误,不能重建二叉树!");

}

// 4、根据该索引分别得到左右子树的长度

int lLength = index - iL;

int rLength = iR - index;

// 5、构建出左右子树的前、中序遍历序列,从而分别得到左右子树

root.left = construct(pre, pL + 1, pL + lLength, in, iL, index - 1);

root.right = construct(pre, pL + lLength + 1, pR, in, index + 1, iR);

return root;

}

}

6 二叉树的下一个节点

题目

给定一棵二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向左、右节点的指针,还有一个指向父节点的指针。

思路

可以分成两种情况:

如果该节点有右子树,那么它的下一个节点就是它的右子树中的最左子节点;

如果该节点没有右子树,则向上遍历,直到找到第一个这样一个父节点:当前节点所在的子二叉树为该父节点的左子节点。如果没有找到该父节点,说明当前节点没有下一个节点。

代码

/*

public class TreeLinkNode {

int val;

TreeLinkNode left = null;

TreeLinkNode right = null;

TreeLinkNode next = null;

TreeLinkNode(int val) {

this.val = val;

}

}

*/

public class Solution {

public TreeLinkNode GetNext(TreeLinkNode pNode) {

// 特殊输入

if (pNode == null) {

return null;

}

TreeLinkNode cur = null;

// 第一种情况:当前节点有右子树

if (pNode.right != null) {

// 获取右子树的根节点

cur = pNode.right;

// 遍历得到最左子节点

while (cur.left != null) {

cur = cur.left;

}

return cur;

}

// 第二种情况:当前节点没有右子树

cur = pNode;

TreeLinkNode parent = cur.next;

// 特殊考虑:当前节点为根节点——没有父节点

while (parent != null && parent.left != cur) {

cur = parent;

parent = parent.next;

}

// 存在与否都返回parent

return parent;

}

}

7 用两个栈实现队列

题目

用两个栈实现一个队列。

思路

两个栈分别对应于不同的功能:

一个栈用于入队:直接压入。

另一个栈用于出队:当没有数据是需要从另一个栈倒入数据;有数据是直接弹出。

代码

import java.util.Stack;

public class Solution {

Stack stack1 = new Stack();

Stack stack2 = new Stack();

public void push(int node) {

stack1.push(node);

}

public int pop() {

if (stack2.isEmpty()) {

if (stack1.isEmpty()) { // 如果两个栈中都没有数据,说明队列为空

throw new RuntimeException("Empty!");

}

while (!stack1.isEmpty()) { // 如果stack1不为空,则从中倒腾数据

stack2.push(stack1.pop());

}

}

// 有数据后,又可以直接弹出了

return stack2.pop();

}

}

剑指offer最新版_剑指Offer——Java版本(持续更新)相关推荐

  1. 剑指offer最新版_剑指offer第二版速查表

    5.替换空格:python直接替换 6.从尾到头打印链表: 借助栈或直接利用系统调用栈 // 创建链表(设置next节点时就会创建下一个节点), 打印链表(最后打印nil) xxx8.二叉树的下一个节 ...

  2. 应届生offer长什么样_你的offer长什么样? 拿到offer就是被录取了吗?

    原标题:你的offer长什么样? 拿到offer就是被录取了吗? 我们普遍认为的是:只要大学发了录取offer ,那么就一定是能够入学的.然而,事情没有那么简单.通常情况下,offer是录取的意思,但 ...

  3. 剑指offer python实现_剑指offer系列python实现 日更(三)

    今天来讲讲斐波那契数列和它的孩子们~先讲个冷笑话:今天来一盘斐波那契炒饭,它等于昨天的炒饭加上前天的炒饭 ‍ 7.斐波那契数列 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第 ...

  4. python剑指offer面试题_剑指Offer(Python语言)面试题38

    面试题38:字符串的排列 题目:输入一个字符串,打印出该字符串中字符的所有排列.例如,输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca和cba. # - ...

  5. 剑指offer python实现_剑指Offer第2题详解(附Python、Java代码实现)

    题目描述 请实现一个函数,将一个字符串中的每个空格替换成"%20".例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. 这个题较为 ...

  6. python剑指offer面试题_剑指offer面试题Q10 斐波那契数列 python解法

    Q10.斐波那契数列 题目描述 写一个函数,输入n,求斐波那契数列的第n项. 解题思路 思路一 递归 递归很简单但是并不能AC python实现代码 class Solution: def Fibon ...

  7. 剑指offer没有java版吗_剑指Offer(Java版) 持续更新中

    面试题2 单例(之前有整理,略) 面试题3 二维数组中的查找 public boolean find(int target, int [][] array) { boolean found = fal ...

  8. java八股文指的是什么_八股文指的是什么意思(带你全面认识八股文)

    八股文,是明清科举考试的一种文体,也称制义.制艺.时文.八比文.八股文章就四书五经取题,内容必须用古人的语气,绝对不允许自由发挥,而句子的长短.字的繁简.声调高低等也都要相对成文,字数也有限制. 八股 ...

  9. 深度学习 用户画像_用户画像架构方案(持续更新)

    说明:用户画像的概念以及意义不在此赘述,这里只探讨如何快速搭建基础架构以及后续工作的注意事项. 用户画像的提出是基于日益发展的业务需要,在相对充分的数据储备之上的进一步理解和提炼数据过程中提出的概念. ...

最新文章

  1. Caffe 编译安装
  2. Oracle中常用的命令,随着学习进度总结
  3. 彩超探头频率高低的区别_超声波液位开关和液位开关的区别,它们的工作原理分别是什么?...
  4. 库克的采访给我们带来的思考!
  5. string与char*的转换(转载)
  6. el-popover超过固定高度后出现滚动条_「测绘精选」RTK测量不出现固定解的原因...
  7. 高并发与负载均衡-keepalived-概念介绍
  8. (转)Java线程:新特征-线程池
  9. dubbo源码1-暴露服务
  10. winscp如何连接安卓手机_通过winscp连接路由器
  11. 工业机器人技术全解析,值得收藏!
  12. VRF proof极简理解
  13. CanOpen通信协议python实现
  14. 计算机绘制函数的应用,信息技术应用用计算机绘制函数图象 (4).pptx
  15. CobaltStrike优质学习资源
  16. 喜讯!双驰企业正式成为欧盟地平线2020 项目合作伙伴
  17. 世界首个拥有肌肉骨骼机器人问世(组图)
  18. 实验三:跟踪分析Linux内核的启动过程 ----- 20135108 李泽源
  19. 微信昵称中表情保存到数据库问题
  20. qq能正常使用 网页打不开的解决办法

热门文章

  1. Web前端工作笔记013---拦截所有的ajax请求,设置出错信息
  2. GetTickCount() 函数的作用和用法(转)
  3. memset详解 设置无穷大INF
  4. php计算器如何保留输入数字,php如何实现计算器代码
  5. 3d激光雷达开发(voxel滤波)
  6. 电子计算机工作的特征是什么,电子计算机的基本特征是什么?
  7. java replace第二个_java - 错误的第二个参数类型:从片段内调用.replace() - 堆栈内存溢出...
  8. 河南科技学院去年对口计算机分数线,河南科技学院录取分数线2021是多少分(附历年录取分数线)...
  9. 增广最小二乘法 matlab 东南大学,各种最小二乘法总结(算法+matlab源码)
  10. android的辅助代码,跟App相关的Android辅助类