剑指Offer
1-5
1.赋值运算函数
- 暂略
2.实现Singleton模式
设计一个类,我们只能生成该类的一个实例。
-
饿汉式
- 优点:没有加锁,执行效率会提高,线程安全。
- 缺点:类加载时就初始化,浪费内存。
-
懒汉式
-
线程不安全
- getInstance时实例为空则创建,否则返回。
- 多线程环境下会导致实例的多次创建。
-
线程安全
-
在方法上添加synchronized
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:影响效率。
-
双重校验锁*
if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); ...
-
-
-
静态内部类*
private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static final Singleton getInstance() { return SingletonHolder.INSTANCE; ...
-
枚举*
public enum Singleton { INSTANCE; public void whateverMethod() { ...
- 简洁,线程安全,自动支持序列化机制。
3.二维数组中的查找
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
-
左下/右上
- 从左下角或右上角开始查找,例如左下
now>target:column–
else:row++
- 从左下角或右上角开始查找,例如左下
4.替换字符串中的空格
请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.”,则输出“We%20are%20happy.”。
- StringBuilder.append
5.从尾到头打印链表
输入一个链表的头结点,从尾到头反过来打印出每个结点的值。
-
栈
- 从头到尾入栈,出栈输出
-
递归
- 递归输出下一节点值
6-10
6.重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
- 递归
- 前序遍历中第一个节点是根节点
中序遍历中根节点左边为左子树,右边为右子树
7.用两个栈实现队列
用两个栈实现一个队列。分别完成在队列尾部插入结点和在队列头部删除结点的功能。
- push:stack1正常push操作
pop:stack2 从1中转移所有数据后执行pop操作
8.旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。
-
二分法
- 首位指针1,2
mid > mid+1 return mid+1
mid < mid-1 return mid
若mid>1,mid位于前半数组,min在后,1=mid+1
反之亦然 - mid>right left=mid+1
else right = mid
return left
- 首位指针1,2
9.斐波那契数列
-
递归
-
循环
- a[i] = a[i-1] + a[i-2];
-
一只青蛙一次可以跳上1 级台阶,也可以跳上2 级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
-
用 2×1的小矩形横着或者竖着去覆盖更大的矩形。请问用8个2×1的小矩形无重叠地覆盖一个2×8的大矩形,总共有多少种方法?
10.二进制中1的个数
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。
-
正数
- a&1!=0
右移1位
- a&1!=0
-
考虑负数
- 1100-1=1011
1011&1100=1000 - a&a-1 将a最右边的1变成0
直到a=0
- 1100-1=1011
11-15
11.数值的整数次方
实现函数 double Power(double base, int exponent),求 base的exponent次方。
- 计算机表示小数(float/double)都有误差,不能直接用等号(==)判断两个小数是否相等.
如果两个小数的差的绝对值很小,比如小于0.0000001,就可以认为它们相等.
12.打印1到最大的n位数*
输入数字n,按顺序打印出从1最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数即999。
- 考虑大数Long型溢出
字符串模拟加法 - 将问题转化为n次0-9的全排列
(0) 1 2 3 4 5 6 7 8 9
’1‘0 ’1‘1 ’1‘2 ’1‘3 …
使用递归
13.在O(1)时间删除链表结点
给定单向链表的头指针和一个结点指针,定义一个函数在 O(1)时间删除该结点。
- 正常情况 将下一个结点值赋给当前结点 并删除下一结点
- 当链表只有一个结点 删除并赋空
- 当被删除结点位于链表尾 则遍历删除
14.调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
- 首尾指针 互换奇偶
15.链表中倒数第k个结点
输入一个链表,输出该链表中倒数第 k 个结点。
-
设置两个指针 第一个指针先走k-1步 然后两个指针一起走 直到第一个指针走到末尾
-
注意程序鲁棒性
-
举一反三
- 求链表的中间结点。如果链表中结点总数为奇数,返回中间结点;如果结点总数是偶数,返回中间两个结点的任意一个。为了解决这个问题,我们也可以定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。当走得快的指针走到链表的末尾时,走得慢的指针正好在链表的中间。
- 判断一个单向链表是否形成了环形结构。和前面的问题一样,定义两个指针,同时从链表的头结点出发,一个指针一次走一步,另一个指针一次走两步。如果走得快的指针追上了走得慢的指针,那么链表就是环形链表;如果走得快的指针走到了链表的末尾(m_pNext指向NULL)都没有追上第一个指针,那么链表就不是环形链表。
16-20
16.反转链表
定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
- pre node next newHead
next = node.next
node.next = pre
pre = node
node = next
17.合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的。
- 两个指针指向两个链表头部
递归比较最小值插入新链表 - 建立新链表
将较小值插入新链表中
18.树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。
- 递归遍历找出与根结点相同的结点
继续递归子结点是否相同
19.二叉树的镜像
完成一个函数,输入一个二叉树,该函数输出它的镜像。
- 递归前序遍历交换左右结点
20.顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
- 循环 注意临界条件
左到右 i = c1; i <= c2;
上到下 j = r1+1; j <= r2;
右到左 i = c2-1; i > c1;
下到上 j = r2; j > r1;
21-25
21.包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数。
- 使用一个辅助栈 存放最小值
22.栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。
- 使用辅助栈 存放弹出顺序
主栈按顺序存入压入序列 当主.peek == 辅.peek一起弹出
23.从上往下打印二叉树
从上往下打印出二叉树的每个结点,同一层的结点按照从左到右的顺序打印。
- 层序遍历 利用队列 同有向图的广度优先遍历
24.二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。
- 后序遍历中 最后一位是根节点 比根节点小的是左子树 递归
25.二叉树中和为某一值的路径*
输入一棵二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。
- 前序遍历
若当前结点为叶子结点 且值相等 则保存路径
回退时要减去当前值
26-30*
26.复杂链表的复制
在复杂链表中,每个结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling 指向链表中的任意结点或者NULL。
- 首先将在原链表的基础上将每个结点复制一份
如:1->1’`->2->2’->3->3’ - 复制指向任意结点的指针
如:1->3 1’->3’ - 将链表按奇偶分离
27.二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
- 二叉搜索树的中序遍历为排序遍历
- 递归思想 将左子树中序遍历 左子树中最大值 与根节点连接 右子树中最小值 与根节点连接
28.字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
- 递归将当前字符与前一个字符位置交换
- 输入一个含有8个数字的数组,判断有没有可能把这8个数字分别放到正方体的8个顶点上(如图4.15所示),使得正方体上三组相对的面上的4个顶点的和都相等。
- 在8×8的国际象棋上摆放8个皇后,使其不能相互攻击,即任意两个皇后不得处在同一行、同一列或者同一对角线上。
29.数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
- 遍历数组 从首位开始 遇到相同的数字count+1 不同的数字 count-1 若count=0 换下一个数字
最后判断是够超过一半
30.最小的k个数
输入n个整数,找出其中最小的k个数。
-
使用快速排序 直到标准值等于k 则前半段为结果
-
建立k大小的最大堆 依次填充至满 继续比较堆最大值与数组中其他数字的大小 若小于则替换
-
最大堆
- 完全二叉树 可以在数组中表示
其中a[0]为根节点/最大值 - 保证所有父节点大于等于子节点
- i结点的父结点下标为(i–1)/2
左右子结点下标为2 * i + 1和2 * i + 2 - 增加 直接将新增值加入末尾 然后更新堆
比较与父节点的大小 若大于父节点 则交换位置 - 删除 只删除堆顶值
将数组首尾互换 即最大值最小值互换 然后更新堆 将根结点与子节点中较大值互换 最后删除原最大值
- 完全二叉树 可以在数组中表示
-
31-35
31.连续子数组的最大和
输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
- 动态规划
- 创建目标数组长度大小的新数组存放i位最大值
即 i-1时的最大值+i 与 i 的较大值
32.从1到n整数中1出现的次数*
输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。
-
分析
- 如果第i位(从低位向高位开始)上的数字是0,那么第i位可能出现1的次数仅由更高位决定(如果没有高位,则视高位为0),等于更高位数字*当前位数的权重10^(i-1);
- 如果第i位上的数字为1,则第i位上可能出现1的次数不仅受更高位影响还受低位影响(如果没有低位,则视低位为0),等于更高位数字*当前位数的权重10^(i-1) + (低位数字+1);
- 如果第i位上的数字大于1,则第i位上可能出现1的次数仅由更高位决定(如果没有高位,则视高位为0),等于(更高位数字+1)*当前位数的权重10^(i-1)。
-
解法
-
取第 i 位左边的数字(高位),乘以 10 ^(i−1) ,得到基础值 a 。
-
取第 i 位数字,计算修正值:
- 如果大于 1,则结果为 a+ 10 ^(i−1) 。
- 如果小于 1,则结果为 a 。
- 如果等于 1,则取第 i 位右边(低位)数字,设为 b ,最后结果为 a+b+1 。
-
33.把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
- 将数组转为字符串数组 重写sort的Comparator方法
比如 a,b 若ab>ba 则a比b大
Arrays.sort(strs, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
String s1 = o1 + o2;
String s2 = o2 + o1;
return s1.compareTo(s2);
...
34.丑数
我们把只包含因子2、3和5的数称作丑数(Ugly Number)。求按从小到大的顺序的第1500个丑数。
- 按顺序查询数字是否是丑数
循环被2/3/5除 == 1
返回第1500个丑数 - 建立丑数数组 保存所有前1500个丑数
每个丑数都可以看成是由1乘n个2/3/5而来
从1开始 取2/3/5倍数中的最小值放入数组中
取过的倍数移到下一个丑数数组中的元素
35.第一个只出现一次的字符
在字符串中找出第一个只出现一次的字符。如输入"abaccdeff",则输出’b’。
- HashMap
36-40
36.数组中的逆序对*
在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
-
归并排序
- 将单个数组排序转化为两个有序数据整合
- 将数组循环二分 直到为单个元素
再两两结合 进行排序
-
在比较时加入全局变量count
data[start]>=data[index]
count = mid+1-start
37.两个链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。
-
将两个链表放入两个栈中
弹出直到不相等
上一个弹出的值就是第一个公共结点 -
得到两个结点的长度
两个指针指向链表头部
长的链表指针先走长度差
然后一起走 第一个相同的结点就是公共结点- 不需要额外空间
38.数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。例如输入排序数组{1,2, 3,3,3,3,4,5}和数字3,由于3在这个数组中出现了4次,因此输出4。
- 二分查找第一个目标数字与最后一个目标数字
39.二叉树的深度
输入一棵二叉树的根结点,求该树的深度。
-
二叉树深度为左右子树中深度较大值+1
private static int getDepth(Tree root) { if (root == null) return 0; int left = getDepth(root.left); int right = getDepth(root.right); return left > right ? left + 1 : right + 1; ...
-
输入一棵二叉树的根结点,判断该树是不是平衡二叉树。
- 获取每棵子树是否平衡
if (root == null) return true;
boolean condition = Math.abs(getDepth(root.left) - getDepth(root.right)) <= 1;
return condition && isBalanced(root.left) && isBalanced(root.right);
40.数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是 O(n),空间复杂度是O(1)。
-
a 异或/^ a = 0
a 异或/^ 0 = a
xxx1 & 1 = 1- 得到数组异或值 则该值为两个单次树异或值
- 通过&获得结果最后1的位数
- 将数组分为该位数为1/不为1两组 分别异或
40-45
41.和为s的两个数字VS和为s的连续正数序列
-
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,输出任意一对即可。
- 首尾指针
大于目标值尾指针向前
小于目标值首指针向后
- 首尾指针
-
输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1~5、4~6和7~8。
- 在数组开始顺次两个指针
指向目标序列头尾
小于目标值则尾指针向后
大于则头指针向后
- 在数组开始顺次两个指针
42.翻转单词顺序 VS左旋转字符串
-
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student.",则输出"student.a am I"。
- 先翻转整个字符串
然后按空格分成字符数组
顺次翻转
- 先翻转整个字符串
-
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"。
- 先翻转整个字符串
再分开翻转后两个与后两个之前
- 先翻转整个字符串
43.n个骰子的点数*
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
- 用n*6长度的数组保存骰子的点数的次数
44.扑克牌的顺子
从扑克牌中随机抽 5张牌,判断是不是一个顺子,即这 5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王可以看成任意数字。
- 用数组记录五张牌 排序后
若空缺位置<=0(大小王)的个数
成顺子 - 若有除0外的重复牌 不成
45.圆圈中最后剩下的数字*
0,1,…,n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
-
公式
- f[n] = (f[n-1]+k) mod n
-
循环链表模拟
46-50
46.求1+2+…+n
求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
private static int sum(int n) {
int sum = n;
boolean result = (n > 0) && ((sum += sum(n - 1)) > 0);
return sum;
...
47.不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、×、÷四则运算符号。
private static int add(int a, int b) {
while (b != 0) {
int temp = a ^ b;
b = (a & b) << 1;
a = temp;
}
return a;
...
48.不能被继承的类
- java可以用final
49.把字符串转换成整数
- 注意特殊情况
50.求两个结点的最低公共祖先
-
二叉搜索树
- 若两个结点的值都小于根节点
则两个结点位于左子树
同理
直到一左一右
- 若两个结点的值都小于根节点