剑指offer(39-52)
前言
- String的 + 运算用 StringBuild的 append() 替代
- 所有存在比较的地方都可以转化为“阴阳”
- 二分法不只是可以用来比较数字大小
- 归并排序的前后调换也可以用来计数
- 所有的辅助空间都可以考虑重复使用
39. 数组中出现次数超过一半的数字
【题目】
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
进阶问题: 给定一个整型数组 arr,再给定一个整数 K,打印所有出现次数大于 N/K 的数,如果没有这样的数,打印提示信息。
【思路】
方法1:哈希表记录每个数及其出现的次数,空间复杂度O(N)
方法2:一次在数组中删掉 K 个不同的数,不停地删除,直到剩下数的种类不足 K 就停止删除,那么,如果一个数在数组中出现的次数大于 N/K,则这个数最后一定会被剩下来。
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length == 0){
return 0;
}
//筛选候选
int cnd = 0;
int times = 0;
for(int i = 0; i < array.length; i++){
if(times == 0){
cnd = array[i];
times = 1;
}else if(array[i] == cnd){
times++;
}else{
times--;
}
}
//验证候选
times = 0;
for(int i = 0; i < array.length; i++){
if(array[i] == cnd){
times++;
}
}
return times > array.length / 2 ? cnd : 0;
}
}
40. 最小的 K 个数
【题目】
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
要求实现时间复杂度为 O(Nlogk)和 O(N)的方法
【思路】
O(Nlogk):大小为k的大根堆
O(N):BFPRT 算法 改进版partition
//大根堆:
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> result = new ArrayList<>();
if(k < 1 || input.length < k){
return result;
}
int[] heap = new int[k];
for(int i = 0; i < k; i++){
heapInsert(heap, input[i], i);
}
for(int i = k; i < input.length; i++){
if(heap[0] > input[i]){
heap[0] = input[i];
heapfiy(heap, 0, k);
}
}
for(int i : heap){
result.add(i);
}
return result;
}
public void heapInsert(int[] heap, int value, int index){
heap[index] = value;
while(index != 0){
int parent = (index - 1) / 2;
if(heap[index] > heap[parent]){
swap(heap, index, parent);
index = parent;
}else{
break;
}
}
}
public void heapfiy(int[] heap, int index, int heapSize){
int left = index * 2 + 1;
while(left < heapSize){
int largest = left + 1 < heapSize && heap[left + 1] > heap[left] ? left + 1 : left;
largest = heap[largest] > heap[index] ? largest : index;
if(largest == index){
break;
}
swap(heap, index, largest);
index = largest;
left = index * 2 + 1;
}
}
public void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
//大根堆:api实现
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
if(k < 1 || input.length < k){
return new ArrayList<>();
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for(int i = 0; i < input.length; i++){
maxHeap.add(input[i]);
if(maxHeap.size() > k){
maxHeap.poll();
}
}
return new ArrayList<>(maxHeap);
}
}
//3区域partition
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> result = new ArrayList<>();
if(k < 1 || input.length < k){
return result;
}
int[] copyArr = new int[input.length];
System.arraycopy(input, 0, copyArr, 0, input.length);
findKthNum(copyArr, k - 1);
for(int i = 0; i < k; i++){
result.add(copyArr[i]);
}
return result;
}
public void findKthNum(int[] arr, int k){
int L = 0;
int R = arr.length - 1;
while(L < R){
int[] equal = partition(arr, L, R);
if(k >= equal[0] && k <= equal[1]){
break;
}else if(k > equal[1]){
L = equal[1] + 1;
}else {
R = equal[0] - 1;
}
}
}
public int[] partition(int[] arr, int L, int R){
int p = arr[R];
int less = L - 1; //小于p的最后一个位置
int more = R; //大于p的的第一个位置
while(L < more){ // L代表了等于P的最后一个位置
if(arr[L] < p){
swap(arr, ++less, L++); //【如果L小于p,则与less后一个等于p的交换】
}else if(arr[L] > p){
swap(arr, --more, L);
}else{
L++;
}
}
swap(arr, R, more++); // 【错误处】 此处交换应该让大于p的边界回退一位
return new int[]{less + 1, more - 1};
}
public void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
41.1 数据流中的中位数
【题目】
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
【思路】
基础班:一个大根堆,一个小根堆,如果其中一个的size比另一个多2,则弹出多的那一个的堆顶到另一个去
改进版:设置一个变量,控制两个堆的平衡,保证其中一个的size始终不小于另一个,省去调整的步骤
//基础版
import java.util.*;
public class Solution {
private PriorityQueue<Integer> largeHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
private PriorityQueue<Integer> lessHeap = new PriorityQueue<>();
public void Insert(Integer num) {
if(largeHeap.size() == 0 || lessHeap.size() == 0){ //【错误处】:没有考虑第一个插入的情况
largeHeap.add(num);
}else if(num <= largeHeap.peek()){
largeHeap.add(num);
}else{
lessHeap.add(num);
}
adjust();
}
public void adjust(){
if(largeHeap.size() - lessHeap.size() > 1){
lessHeap.add(largeHeap.poll());
}else if(lessHeap.size() - largeHeap.size() > 1){
largeHeap.add(lessHeap.poll());
}else{
return;
}
}
public Double GetMedian() {
if(lessHeap.size() == largeHeap.size()){
return new Double(((double)(lessHeap.peek() + largeHeap.peek())) / 2);
}else if (lessHeap.size() > largeHeap.size()){
return new Double((double) lessHeap.peek());
}else{
return new Double((double) largeHeap.peek());
}
}
}
//改进版
import java.util.*;
public class Solution {
private PriorityQueue<Integer> largeHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
private PriorityQueue<Integer> lessHeap = new PriorityQueue<>();
private boolean isLess = true;
public void Insert(Integer num) {
if(isLess){
// 【易错点】直接插入无法保证右边的都比左边大
largeHeap.add(num);
lessHeap.add(largeHeap.poll());
}else{
lessHeap.add(num);
largeHeap.add(lessHeap.poll());
}
isLess = !isLess;
}
public Double GetMedian() {
if(lessHeap.size() == largeHeap.size()){
return new Double( (lessHeap.peek() + largeHeap.peek()) / 2.0);
}else{
return new Double( (double) lessHeap.peek());
}
}
}
41.2 字符流中第一个不重复的字符
【题目】
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
如果当前字符流没有存在出现一次的字符,返回#字符。
【思路】
用 map 存 key-value;
用 queue 或 stack 存顺序
import java.util.*;
public class Solution {
private HashMap<Character, Integer> map = new HashMap<>();
private Queue<Character> queue = new LinkedList<>();
//Insert one char from stringstream
public void Insert(char ch)
{
if(map.containsKey(ch)){
map.put(ch, map.get(ch) + 1);
queue.add(ch);
}else{
map.put(ch, 1);
queue.add(ch);
}
while(!queue.isEmpty() && map.get(queue.peek()) > 1){
queue.poll();
}
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
return queue.isEmpty() ? '#' : queue.peek(); //【错误处】 写为 poll()
}
}
42. 连续子数组的最大和
【题目】
今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?
例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)
【思路】
前面累加和小于等于0时则丢弃,用当前值取代,遍历过程中用max变量记录累加和的最大值
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
if (array == null || array.length == 0) {
throw new RuntimeException();
}
int sum = 0;
int max = Integer.MIN_VALUE;
for(int i = 0; i < array.length; i++){
sum = sum <= 0 ? array[i] : array[i] + sum; //应该抛去前面的负数,而不是后面的
//sum = sum + array[i] > 0 ? sum + array[i] : 0; //【错误处】没有考虑到全部为负数的情况
max = Math.max(sum, max);
}
return max;
}
}
43. 从 1 到 n 整数中 1 出现的次数
【题目】
求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
【思路】
暴力法:略
数学规律:
- 规律1:
1~10,在个位上,1出现 1 次;
1~100,在十位上,1出现 10 次;
1~1000,在百位上,1出现 100 次
依此类推,1~10^i ,1出现 10^(i - 1) 次;
- 规律2:
以2104 举例:
个位上:从1~2104,包含210个10,个位上的4大于1,则有 (210 + 1)* 1;
十位上:从1~2104,包含21个100,十位上0小于1,则有 (21 + 0)* 10;
百位上:从1~2104,包含2个1000,百位上1等于1(决定1的数为百位之后的和100本身),则有(21 + 0)*100 + (4 + 1);
。。。
- 总结:
对于数字n,计算它的第i(i从1开始,从右边开始计数)位数上包含的数字1的个数:
假设第i位上的数字为x的话,则
1.如果x > 1的话,则第i位数上包含的1的数目为:(高位数字 + 1)* 10 ^ (i-1) (其中高位数字是从i+1位一直到最高位数构成的数字)
2.如果x < 1的话,则第i位数上包含的1的数目为:(高位数字 )* 10 ^ (i-1)
3.如果x == 1的话,则第i位数上包含1的数目为:(高位数字) * 10 ^ (i-1) +(低位数字+1) (其中低位数字时从第i - 1位数一直到第1位数构成的数字
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
if(n <= 0){
return 0;
}
int ones = 0;
for(int i = 1; i <= n; i = 10 * i){
int high = n / (10 * i);
int mod = n % (10 * i);
int cur = mod / i;
int low = mod % i;
ones += high * i;
if(cur == 1){
ones += low + 1; //【错误处1】:未加 1
}
if(cur > 1){
ones += i;
}
}
return ones;
}
}
44. 数字序列中的某一位数字
【题目】
数字以 0123456789101112131415… 的格式序列化到一个字符串中,求这个字符串的第 index 位。
【思路】
根据 index 从 1位开始递归
index=0:0 1个
一位数:1-9 9个; 9 * 1
两位数:10-99 90个; 90 * 2
三位数:100-999 900个; 900 * 3
四位数:1000-9999 9000个; 9000 * 4 10^(4-1)+
class Solution {
public int getDigitAiIndex(int index){
if(index < 0){
return -1;
}
int place = 1;
while(true){
int amount = getAmountOfPlace(place);
if(index < amount){
return getDigitAtIndex(index, place);
}
index -= amount;
place++;
}
}
public int getAmountOfPlace(int place){
if(place == 1){
return 10;
}
return (int) Math.pow(10, place - 1) * 9 * place;
}
public int getBeginNumberOfPlace(int place){
if(place == 1){
return 0;
}
return (int) Math.pow(10, place - 1);
}
public int getDigitAtIndex(int index, int place){
int beginNumber = getBeginNumberOfPlace(place);
int shiftNumber = index / place;
int mod = index % place;
String number = (beginNumber + shiftNumber) + "";
return number.charAt(mod) - '0';
}
}
45. 把数组排成最小的数
【题目】
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
【思路】
暴力:全排列,比较大小
改进:可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。
问题:两个int型整数的拼接可能造成数字溢出,应该用字符串表示,比较时直接按照字符串比较,因为位数相同
import java.util.ArrayList;
import java.util.*;
public class Solution {
public String PrintMinNumber(int [] numbers) {
if(numbers == null || numbers.length == 0){
return "";
}
String[] nums = new String[numbers.length];
for(int i = 0; i < nums.length; i++){
nums[i] = String.valueOf(numbers[i]);
}
Arrays.sort(nums, (o1, o2) -> (o1 + o2).compareTo(o2 + o1));
StringBuilder result = new StringBuilder();
for(String str : nums){
result.append(str);
}
return result.toString();
}
}
46. 把数字翻译成字符串
【题目】
给定一个数字,按照如下规则翻译成字符串:0 翻译成“a”,1 翻译成“b”… 25 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。
【思路】
动态规划,类似上台阶,只是加了限制条件
计算dp[i]时:
- s[i-1] < 2,则 dp[i] = dp[i - 1] + dp[i - 2] (说明可以拼接也可以单独翻译)
- s[i-1] = 2,且 s[i] <= 5,则 dp[i] = dp[i - 1] + dp[i - 2] (说明可以拼接也可以单独翻译)
- dp[i] = dp[i - 1] (说明不能拼接,只能单独翻译)
因为只与前一个相关,可以做到空间复杂度 O(1)
public class Demo {
public static int numDecordings (String s) {
if(s == null || s.length() == 0) {
return 0;
}
int prePre = 1;
int pre = 1;
int cur = 0;
for(int i = 1; i < s.length(); i++){
if(s.charAt(i - 1) < '2' || (s.charAt(i - 1) == '2' && s.charAt(i) <= '5')){
cur = prePre + pre;
}else{
cur = pre;
}
prePre = pre;
pre = cur;
}
return cur;
}
public static void main(String[] args) {
String s = "12258";
System.out.println(numDecordings(s));
}
}
47. 礼物的最大价值
【题目】
在一个 m*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于0) 。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘 ,礼物的最大价值为 1+12+5+7+7+16+5=53 .
1 10 3 8
12 2 9 6
5 7 4 11
3 7 16 5
【思路】
动态规划问题
因为只能向右或向下,则最左一列和最上一列是固定的;
dp[ i ] [ j ] = max(dp [i-1] [j], dp [i] [j - 1]) + matrix [i] [j];
//空间 O(M * N)
import java.util.*;
public class Bonus {
public int getMost(int[][] board) {
// write code here
if(board == null || board.length == 0 || board[0].length == 0){
return 0;
}
int rows = board.length;
int cols = board[0].length;
int[][] dp = new int[rows][cols];
dp[0][0] = board[0][0];
for(int i = 1; i < cols; i++){
dp[0][i] = board[0][i] + dp[0][i - 1];
}
for(int i = 1; i < rows; i++){
dp[i][0] = board[i][0] + dp[i - 1][0];
}
for(int i = 1; i < rows; i++){
for(int j = 1; j < cols; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + board[i][j];
}
}
return dp[rows - 1][cols - 1];
}
}
//改进存储空间 O(N)
import java.util.*;
public class Bonus {
public int getMost(int[][] board) {
// write code here
if(board == null || board.length == 0 || board[0].length == 0){
return 0;
}
int rows = board.length;
int cols = board[0].length;
int[] up = new int[cols];
for(int i = 0; i < cols; i++){
if(i == 0){
up[i] = board[0][i];
}else{
up[i] = board[0][i] + up[i - 1]; // 【错误处】:没有加前面的
}
}
for(int i = 1; i < rows; i++){
up[0] = up[0] + board[i][0];
for(int j = 1; j < cols; j++){
up[j] = Math.max(up[j - 1], up[j]) + board[i][j];
}
}
return up[cols - 1];
}
}
48. 最长不含重复字符的子字符串
【题目】
输入一个字符串(只包含 a~z 的字符) ,求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。
【思路】
暴力法:略
动态递归:
令 f(i) 为 包含 i 位置的最大不重复子串长度;
如何通过 f(i - 1) 得到 f(i) ? 即如何构造条件让状态能够转移
- 本题的限制条件在于 f(i - 1) 所包含的 字符中可能和 当前 i 位置重复,可以构建hash表记录 上一次 出现 i 位置字符的 坐标,从而判断是否被 f(i - 1) 的范围所包含
- 构建状态转移方程,假如上一次 i 出现的位置与 i 当前位置的举例为 d:
- f(i) = f(i - 1) + 1 , d > f(i - 1) 时;
- f(i) = d , d <= f(i - 1) 时
- 用全局变量 max 记录最大的 f(i);
import java.util.Arrays;
public class Demo {
public static int longestSubStringWithoutDuplicaton (String str){
if(str == null || str.length() == 0){
return 0;
}
int[] preIndex = new int[26];
Arrays.fill(preIndex, -1);
int maxLength = 0;
int curLength = 0;
for(int i = 0; i < str.length(); i++){
int c = str.charAt(i) - 'a';
int preI = preIndex[c];
if(preI == -1 || i - preI > maxLength){
curLength += 1;
}else {
curLength = i - preI;
}
preIndex[c] = i;
maxLength = Math.max(maxLength, curLength);
}
return maxLength;
}
public static void main(String[] args){
String s = "arabcacfr";
System.out.println(longestSubStringWithoutDuplicaton(s));
}
}
49. 丑数
【题目】
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
【思路】
import java.util.*;
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index<=0)
return 0;
ArrayList<Integer> list = new ArrayList<Integer>();
//add进第一个丑数1
list.add(1);
//三个下标用于记录丑数的位置
int i2=0,i3=0,i5=0;
while(list.size()<index){
//三个数都是可能的丑数,取最小的放进丑数数组里面
int n2=list.get(i2)*2;
int n3=list.get(i3)*3;
int n5=list.get(i5)*5;
int min = Math.min(n2,Math.min(n3,n5));
list.add(min);
if(min==n2)
i2++;
if(min==n3)
i3++;
if(min==n5)
i5++;
//防止重复计算,相等的全部跳过
}
return list.get(list.size()-1);
}
}
50. 第一个只出现一次的字符位置
【题目】
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
【思路】
hash表/数组,遍历两道,第一道计数,第二道找数
public class Solution {
public int FirstNotRepeatingChar(String str) {
if(str == null || str.length() == 0){
return -1;
}
int[] counts = new int[256];
for(int i = 0; i < str.length(); i++){
counts[str.charAt(i)]++;
}
for(int i = 0; i < str.length(); i++){
if(counts[str.charAt(i)] == 1){
return i;
}
}
return -1;
}
}
51. 数组中的逆序对
【题目】
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。 并将P对1000000007取模的结果输出。 即输出P%1000000007
【思路】
用归并排序的思想,在排序过程中统计前面比后面大的个数;
public class Solution {
private long count = 0;
private int[] tmp;
public int InversePairs(int [] array) {
if(array == null || array.length < 1){
return 0;
}
tmp = new int[array.length];
mergeSort(array, 0, array.length - 1);
return (int) count % 1000000007 ;
}
private void mergeSort(int[] arr, int start, int end){
if(start == end){
return;
}
int mid = start + ((end - start) >> 1);
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
merge(arr, start, mid, end);
}
private void merge(int[] arr, int start, int mid, int end){
int i = mid, j = end, tmpIndex = end;
while(i >= start && j >= mid + 1){
if(arr[i] > arr[j]){
tmp[tmpIndex--] = arr[i--];
count += j - mid;
count %= 1000000007 ; //【错误处】:不加这句会越界
}else {
tmp[tmpIndex--] = arr[j--];
}
}
while(i >= start){
tmp[tmpIndex--] = arr[i--];
}
while(j >= mid + 1){
tmp[tmpIndex--] = arr[j--];
}
for(tmpIndex = start; tmpIndex <= end; tmpIndex++){
arr[tmpIndex] = tmp[tmpIndex];
}
}
}
52. 两个链表的第一个公共结点
【题目】
输入两个链表,找出它们的第一个公共结点。
【思路】
Y型
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead2 == null || pHead1 == null){
return null;
}
int len1 = getLength(pHead1);
int len2 = getLength(pHead2);
ListNode shortList = null;
ListNode longList = null;
int num = 0;
if(len1 >= len2){
longList = pHead1;
shortList = pHead2;
num = len1 - len2;
while(num > 0){
longList = longList.next;
num--;
}
}else{
longList = pHead2;
shortList = pHead1;
num = len2 - len1;
while(num > 0){
longList = longList.next;
num--;
}
}
while(longList != shortList){
longList = longList.next;
shortList = shortList.next;
}
return longList;
}
public int getLength(ListNode head){
int length = 0;
ListNode cur = head;
while(cur != null){
length++;
cur = cur.next;
}
return length;
}
}