剑指Offer-2
Day 1
JZ38:排列组合@next_permutation
排列组合如何通过代码实现?
手撕
next_permtation
标准的“下一个排列”算法可以描述为:
在尽可能靠右的低位进行交换,需要从后向前查找
将一个 尽可能小的「大数」 与前面的「小数」交换,将「大数」换到前面。「大数」后面的所有数需置为升序,升序排列就是最小的排列。
public String[] permutation(String s) {
List<String> res = new ArrayList<String>();
char[] arr = s.toCharArray();
Arrays.sort(arr);//优先得到第一个排列
do {
res.add(new String(arr));
} while (next_permutation(arr));
int size = ret.size();
String[] ans = new String[size];
for (int i = 0; i < size; i++) {
ans[i] = ret.get(i);
}
return ans;
}
public boolean next_permutation(char[] nums) {
int len = nums.length;
for (int i = len - 1; i > 0; i--) {
if (nums[i] > nums[i - 1]) {//首先找到首个相邻递增对
Arrays.sort(nums, i, len);//直接升序
for (int j = i; j < len; j++) {//交换[i-1]与第一个大者
if (nums[j] > nums[i - 1]) {
char temp = nums[j];
nums[j] = nums[i - 1];
nums[i - 1] = temp;
return true;
}
}
}
}
//Arrays.sort(nums);//用于321->123
return false;而组合,也可以用递归考虑。
JZ39:数组中频率超过一半的数字@快速排序与中位数
参考:dsacpp-12.2.2 
记“过半数元素”为众数。设P为向量A中长度为2m的前缀。若元素x在P中恰好出现m次,仅当后缀A-P拥有众数,A有众数,且A-P的众数就是A的众数。若A的众数就是x,则在剪除前缀P之后,x与非众数均减少相同的数目,二者数目的差距在后缀A-P中保持不变。反过来,若A的众数为y,则在剪除前缀P之后,y减少的数目也不致多于非众数减少的数目,二者数目的差距在后缀A-P中也不会缩小。
上面的方法很难想到,但是基于快速排序的partition选取方便想到,在此介绍:
本题其实是选中间位置。
JZ40:数组中最小的k个数
参考上面的快速partition选取,取A[0..k)即可。
=>拓展:如果需要去掉重复元素?P1138 第 k 小整数 - 洛谷
TopK专题
参:dsacpp12.2.6
本节将延续以上快速选取算法的思路,介绍一个在最坏情况下运行时间依然为O(n)的k-选取算法。

上述算法从理论上证实,的确可以在线性时间内完成k-选取。然而很遗憾,其线性复杂度中的常系数项过大,以致在通常规模的应用中难以真正体现出效率的优势。
下面介绍一种更适合处理海量数据、无需修改数组本身的堆局部排序算法,时间复杂度是O(nlogn)。
Java 中提供了现成的 PriorityQueue(默认小根堆),可以直接地处理TopK大问题。重写构造器,
PriorityQueue< >((x, y) -> (y - x)) 可方便实现大根堆。
Day2
JZ41:数据流的中位数@就地堆选取
妙啊,开两个堆。我们把数组存储成[小大根堆|大小根堆]。
JZ43:求1~N中数字1出现的次数
用递归的思维,去找规律。同样适用于以下题目,精妙略。
参考:数位 DP - OI Wiki (oi-wiki.org)
JZ44:数字序列中某一位的数字
JZ45:数组重排粘贴成最小的数
JZ46:数字翻译成字符串
由小到大分析->动态规划
Day3
JZ47:数组中最大和路径@滚动dp
我们发现状态转移的方向不同行且不同列,显然互不干扰,于是:
JZ48:最长不含重复字符的子字符串@滑动窗口
滑动窗口实际上就是双指针,维护一个具有特定性质的“窗口”。
JZ49:丑数@指针就地dp
本题涵盖两种思想。
动态规划的最优子结构思想。
通过指针存储状态,便于比大小的思想。
JZ50: 字符串首个只出现一次的字符@有序哈希
Java 使用 LinkedHashMap 实现有序哈希表。
Day4
JZ51:逆序对数@归并排序
排序,就是在消除逆序对的过程。参考《算法》P170(2.2 归并排序),在此介绍一下。
首先是归并算法。
然后根据逆序对的性质,就是合成时的两个有序数组,左边比右边大的位置差,因此,对于逆序对,其全部产生于第三个判断,有:
if (tmp[j] < tmp[i]) {nums[k] = tmp[j++]; cnt+=mid-i+1;}
JZ52:链表公共节点
思路1:长度差 + 快慢指针。
思路2:注意到从后往前数比较简单,联想到一个很棒的数据结构——后进先出的栈。
通过栈思想,可以达到类似于逆序读入的效果。
JZ53:有序数组频率查询
二分查找的变体。
JZ54:BST的第k大节点@逆中序遍历
方法1:中序遍历求[size-k]的值;
方法2:逆向中序遍历,得到第k个值
JZ55: 是否是AVL
我们提到过,已遍历节点为未遍历节点提供信息。所以本题选择后序遍历(自底向上)。
能否从左右子树的平衡因子,得到根的平衡因子呢?不可以哦。所以我们需要记录深度信息。
Day5
JZ56:数组中数字出现的次数@分组位运算
(1|2,2,2…)对于根据 x ^ x = 0, x ^ y != 0以及异或的结合性,先对全体元素异或,得到的是两个不同元素的数的异或结果。我们要找到最低位的“1”(或任意”1”,对应两个数位不同)进行分组,其掩码与元素们逐个异或判断,即可找到我们想要的数。
下面分析较为复杂的情况:除一个数字只出现一次外,其他数字均出现三次。找出独元素。
参:LeetCode题解Krahets

JZ57:和为s的连续正数序列
剑指 Offer 57 - II. 和为s的连续正数序列 - 力扣(LeetCode)
滑动窗口即可。当 s == target 和 s > target 时移动边界操作相同,因此可以合并。
Day 6
JZ60:n个骰子朝上点数和s概率@dp方向
显然,$dp[n][s] = \sum_1^6dp[n-1][s-i]\times\frac{1}{6}$
我们知道,每个阶段的状态都只和它前一阶段的状态有关,不需要用额外的一维来保存所有阶段。
我们注意到s - i可能会为负数,这里可以考虑换一个状态转移方向:
JZ61:扑克牌中的顺子(王视为Any)
遍历五张牌,满足max - min <= 5。遇到大小王(即 0 )直接跳过。
数字电路系列
用位运算实现加法器等…理解下:程序是个状态机。提示:CSAPP介绍过,位级别一致性。
Day7
JZ66:构建乘积数组

JZ68:二叉树的公共祖先
上述代码通过递归实现了“自底向上”的效果。在返回时,体现了路径的回溯。
接下来还有一种思路,是中序遍历。根据中序遍历的顺序,遍历的顺序一定是(含同时):左节点->公共祖先->右结点。循环时,如果找到其中一个指定节点,其本身作为祖先作为res记录并维护。记录res的当前高度为deep,继续遍历此后的节点,在其下方者均以res为祖先,如果遍历时遇到节点的深度浅于当前记录(向上)则更新,直到遇到第二个指定节点。
将有向无环图中的顶点 v 的高度定义为从根结点到 v 的最长路径。在所有 v 和 w 的共同祖先中,高度最大者就是 v 和 w 的最近共同祖先。LCA 的计算在实现编程语言的多重继承中很有用。参考:http://www.ics.uci.edu/~eppstein/261/BenFar-LCA-00.pdf
最后更新于
这有帮助吗?