寫在前面
- 本系列包含《劍指Offer》66道算法題,預計一週刷完,這是第三篇。
- 所有題目均可在牛客網在線編程平臺進行調試。
網址:https://www.nowcoder.com/ta/coding-interviews - 本系列包含題目,解題思路及代碼(Java)。
代碼同步發佈在GitHub:https://github.com/JohnnyJYWu/offer-Java - 上一篇:算法 | 一週刷完《劍指Offer》 Day2:第17~26題
下一篇:算法 | 一週刷完《劍指Offer》 Day4:第38~50題
Day3:第27~37題
難度上升,多看多想,理解纔好做。
- T27. 字符串的排列
- T28. 數組中出現次數超過一半的數字
- T29. 最小的K個數
- T30. 連續子數組的最大和
- T31. 整數中1出現的次數(從1到n整數中1出現的次數)
- T32. 把數組排成最小的數
- T33. 醜數
- T34. 第一個只出現一次的字符位置
- T35. 數組中的逆序對
- T36. 兩個鏈表的第一個公共結點
- T37. 數字在排序數組中出現的次數
T27. 字符串的排列
題目描述
輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。
輸入描述:輸入一個字符串,長度不超過9(可能有字符重複),字符只包括大小寫字母。
解題思路
同樣是遞歸回溯的思想。先把字符串進行字典序排序,定義hasUsed輔助數組記錄各字符是否使用,然後遞歸對後面的字符排列組合即可。
注意:使用StringBuffer便於字符串操作。每個遞歸結束後記得回溯,去除此循環加入的字符,回退到上一步的排列,與T24中去除節點道理一樣。
private ArrayList<String> result = new ArrayList<>();
public ArrayList<String> Permutation(String str) {
if(str == null || str.length() == 0) return result;
char[] chars = str.toCharArray();
Arrays.sort(chars);//字典序排序
permutation(chars,
new boolean[chars.length],//用於記錄當前字符是否用過
new StringBuffer());//字符串,便於操作
return result;
}
private void permutation(char[] chars, boolean[] hasUsed, StringBuffer str) {
if(str.length() == chars.length) {//長度相同說明出結果,加入result
result.add(str.toString());
return;
}
for(int i = 0; i < chars.length; i++) {
if(hasUsed[i]) continue;
if(i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) continue;//連續兩個值相同時,保證不重複
hasUsed[i] = true;
str.append(chars[i]);
//遞歸對後面的字符進行排列
permutation(chars, hasUsed, str);
//此步重要,去除此循環加入的字符,回退到上一步的排列,與T24中去除節點道理一樣
str.deleteCharAt(str.length() - 1);
hasUsed[i] = false;
}
}
T28. 數組中出現次數超過一半的數字
題目描述
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。例如輸入一個長度爲9的數組{1,2,3,2,2,2,5,4,2}。由於數字2在數組中出現了5次,超過數組長度的一半,因此輸出2。如果不存在則輸出0。
解題思路
多數投票問題。
首先明確,若數字出現次數超過一半,那它必爲出現最多的數字。因此問題轉換爲找出現最多的數字,然後判斷它出現的次數是否超過一半。
定義count來統計一個元素出現的次數,當遍歷到的元素和統計元素不相等時,count --。如果前面查找了 i 個元素,且 count == 0 ,說明前 i 個元素沒有【多數】,或者有【多數】但出現的次數少於 i / 2 ,因爲如果多於 i / 2 的話 count 就一定不會爲 0 。此時剩下的 n - i 個元素中,【多數】的數目依然多於 (n - i) / 2,因此繼續查找就能找出【多數】。
最後,找到多數後再判斷出現次數是否超過一半即可。
public int MoreThanHalfNum_Solution(int[] array) {//多數投票問題
int num = array[0];
int count = 1;
for(int i = 1; i < array.length; i ++) {
if(array[i] == num) {
count ++;
} else {
count --;
}
if(count == 0) {
num = array[i];
count = 1;
}
}
count = 0;
for(int val: array) {
if(val == num) {
count ++;
}
}
return count > array.length / 2 ? num : 0;//三元
}
T29. 最小的K個數
題目描述
輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4,。
解題思路
快速選擇法。快速選擇的總體思路與快速排序一致,選擇一個元素作爲基準來對元素進行分區,將小於和大於基準的元素分在基準左邊和右邊的兩個區域。不同的是,快速選擇並不遞歸訪問雙邊,而是隻遞歸進入一邊的元素中繼續尋找。
快排的 partition() 方法會返回一個整數 j 使得 a[l…j-1] 小於等於 a[j],且 a[j+1…h] 大於等於 a[j],此時 a[j] 就是數組的第 j 大元素。可以利用這個特性找出數組的第 K 個元素。
public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if(k > input.length || k <= 0) return list;
int smallestK = findSmallestK(input, k - 1);
for(int val: input) {
if(val <= smallestK && list.size() < k) {
list.add(val);
}
}
return list;
}
private int findSmallestK(int[] input, int k) {
int low = 0;
int high = input.length - 1;
while(low < high) {
int j = partition(input, low, high);
if(j < k) {
low = j + 1;
} else if(j > k) {
high = j - 1;
} else {
break;
}
}
return input[k];
}
private int partition(int[] nums, int low, int high) {
int i = low;
int j = high + 1;
while(true) {
while(i < high && nums[++ i] < nums[low]) ;
while(j > low && nums[low] < nums[-- j]) ;
if(i >= j) {
break;
}
swap(nums, i, j);
}
swap(nums, low, j);
return j;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
或者,直接排序找最小。。。
public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
ArrayList<Integer> list = new ArrayList<>();
if(k > input.length || k <= 0) return list;
Arrays.sort(input);
for(int i = 0; i < k; i ++) {
list.add(input[i]);
}
return list;
}
T30. 連續子數組的最大和
題目描述
HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全爲正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和爲8(從第0個開始,到第3個爲止)。給一個數組,返回它的最大連續子序列的和,你會不會被他忽悠住?(子向量的長度至少是1)
解題思路
嗯。。。閱讀題,歷來都是題目越長題越簡單。。。單純一點,邊找邊加就行了。
注意查看代碼中唯一的一行註釋,很關鍵。
public int FindGreatestSumOfSubArray(int[] array) {
if(array == null || array.length == 0) return 0;
int sum = 0;
int result = Integer.MIN_VALUE;
for(int val: array) {
if(sum < 0) {
sum = val;//關鍵在此,如果前面n個的和sum已經小於0了,別傻乎乎繼續加,直接從新的val開始吧
} else {
sum += val;
}
if(result < sum) {
result = sum;
}
}
return result;
}
T31. 整數中1出現的次數(從1到n整數中1出現的次數)
題目描述
求出1 ~ 13的整數中1出現的次數,並算出100 ~ 1300?的整數中1出現的次數?爲此他特別數了一下1 ~ 13中包含1的數字有1、10、11、12、13因此共出現6次,但是對於後面問題他就沒轍了。ACMer希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整數區間中1出現的次數(從1 到 n 中1出現的次數)。
解題思路
這種題靠悟性。。。
public int NumberOf1Between1AndN_Solution(int n) {
int ones = 0;
for(int m = 1; m <= n; m *= 10) {
int a = n / m, b = n % m;
if(a % 10 == 0)
ones += a / 10 * m;
else if(a % 10 == 1)
ones += (a / 10 * m) + (b + 1);
else
ones += (a / 10 + 1) * m;
}
return ones;
}
leetcode大神只用了5行的解法,有興趣的深入瞭解一下。。。
https://leetcode.com/problems/number-of-digit-one/discuss/64381/4-lines-olog-n-cjavapython
public int countDigitOne(int n) {
int ones = 0;
for (long m = 1; m <= n; m *= 10)
ones += (n/m + 8) / 10 * m + (n/m % 10 == 1 ? n%m + 1 : 0);
return ones;
}
T32. 把數組排成最小的數
題目描述
輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。例如輸入數組{3,32,321},則打印出這三個數字能排成的最小數字爲321323。
解題思路
可以看做是排序問題,不同點在於此題是比較數字轉換成字符串後相加的大小。
例如兩個數字轉換的字符串S1和S2,應該比較 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那麼應該把 S1 排在前面,否則應該把 S2 排在前面。
public String PrintMinNumber(int[] numbers) {
String[] nums = new String[numbers.length];
for(int i = 0; i < nums.length; i ++) {//int轉string,比較string相加的值
nums[i] = String.valueOf(numbers[i]);
}
Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));//排序,s1+s2與s2+s1兩個字符串比較,誰小誰放前面
String result = "";
for(String str: nums) {
result += str;
}
return result;
}
T33. 醜數
題目描述
把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因爲它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。
解題思路
此題需要思維靈活。由題意,只需不斷從前面已知的醜數中選取合適的醜數分別乘2、3、5,選取最小的醜數加入數組即可。關鍵在於如何選取合適的醜數。(定義2、3、5對應的下標index2、index3、index5,詳見註釋)
public int GetUglyNumber_Solution(int index) {
if(index <= 6) return index;//1~6即爲前6個醜數
int index2 = 0, index3 = 0, index5 = 0;
int[] uglys = new int[index];//存前n個醜數
uglys[0] = 1;//初始化第一個值爲1
int n = 1;//開始計算第二個醜數
while(n < index) {
//找出下一個小的醜數,此步重要需理解,分別用2,3,5在醜數數組裏對應的上一個醜數乘2,3,5找出最小的醜數
int ugly2 = uglys[index2] * 2;
int ugly3 = uglys[index3] * 3;
int ugly5 = uglys[index5] * 5;
int min = Math.min(ugly2, Math.min(ugly3, ugly5));
uglys[n] = min;
n ++;
//將2,3,5對應的上一個醜數後移
if(min == ugly2) index2 ++;
if(min == ugly3) index3 ++;
if(min == ugly5) index5 ++;
}
return uglys[index - 1];
}
T34. 第一個只出現一次的字符位置
題目描述
在一個字符串(0<=字符串長度<=10000,全部由字母組成)中找到第一個只出現一次的字符,並返回它的位置,如果沒有則返回 -1(需要區分大小寫)。
解題思路
char類型一般爲一個字節,範圍在0 ~ 255。因此定義一個整形計數數組int[256],對每個char出現次數進行計數即可。
計數後要按照字符串中的字符順序查找第一個計數次數爲1的字符。
public int FirstNotRepeatingChar(String str) {
int[] array = new int[256];//計數數組
for(int i = 0; i < str.length(); i ++) {
array[str.charAt(i)] ++;
}
for(int i = 0; i < str.length(); i ++) {
if(array[str.charAt(i)] == 1) {//按str的字符順序來,找出第一個計數次數爲1的即爲所求位置
return i;
}
}
return -1;
}
T35. 數組中的逆序對
題目描述
在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數P。並將P對1000000007取模的結果輸出。 即輸出P%1000000007。
解題思路
分治思想,先分後治。先不斷將數組一分爲二,並對這分開的兩部分進行相同操作;然後一邊合併相鄰的子數組,一邊統計逆序對的數目。(實質就是歸併排序的思路)
private long cnt = 0;
private int[] tmp;//輔助數組
public int InversePairs(int[] array) {
tmp = new int[array.length];
mergeSortUp2Down(array, 0, array.length - 1);
return (int) (cnt % 1000000007);
}
private void mergeSortUp2Down(int[] nums, int first, int last) {
if(last - first < 1) return;
int mid = (first + last) / 2;
//分治思想
mergeSortUp2Down(nums, first, mid);
mergeSortUp2Down(nums, mid + 1, last);
merge(nums, first, mid ,last);
}
private void merge(int[] nums, int first, int mid, int last) {
int i = first, j = mid + 1, k = first;
while(i <= mid || j <= last) {
if(i > mid) {
tmp[k] = nums[j];
j ++;
}
else if(j > last) {
tmp[k] = nums[i];
i ++;
}
else if(nums[i] < nums[j]) {
tmp[k] = nums[i];
i ++;
}
else {
tmp[k] = nums[j];
j ++;
this.cnt += mid - i + 1;//nums[i] > nums[j]說明nums[i...mid]都大於nums[j]
}
k ++;
}
for(k = first; k <= last; k ++) {
nums[k] = tmp[k];
}
}
T36. 兩個鏈表的第一個公共結點
題目描述
輸入兩個鏈表,找出它們的第一個公共結點。
解題思路
數學問題。
如圖,鏈表1長度爲 a+c,鏈表2長度爲 b+c。聲明兩個指針node1和node2分別指向兩個鏈表表頭,同步向後移動。
node1走過 a+c 後指空,此時讓它指向鏈表2的表頭並繼續向後走;同理node2走過 b+c 後指向鏈表1表頭。
由於 a+c+b = b+c+a ,此時node1和node2剛好相遇,且相遇在兩個鏈表的第一個公共節點。由此得解。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode node1 = pHead1;
ListNode node2 = pHead2;
while(node1 != node2) {//公共節點後面即爲公共鏈表
if(node1 == null) {
node1 = pHead2;
} else {
node1 = node1.next;
}
if(node2 == null) {
node2 = pHead1;
} else {
node2 = node2.next;
}
}
return node1;
}
T37. 數字在排序數組中出現的次數
題目描述
統計一個數字在排序數組中出現的次數。
解題思路
順序查找。
public int GetNumberOfK(int[] array , int k) {
int sum = 0;
for(int val: array) {
if(val == k) {
sum ++;
}
}
return sum;
}
二分查找,找到第一次和最後一次k出現的位置,即可計算次數。
public int GetNumberOfK(int[] array , int k) {
int first = getFirstK(array, k);
int last = getLastK(array, k);
if(first == -1) return 0;
if(last == -1) return 0;
return last - first + 1;
}
private int getFirstK(int[] array , int k) {
int low = 0, high = array.length - 1;
while (low <= high) {
int mid = (high + low) / 2;
if(array[mid] >= k) {
high = mid - 1;
} else {
low = mid + 1;
}
}
if(low > array.length - 1 || array[low] != k)
return -1;
return low;
}
private int getLastK(int[] array , int k) {
int low = 0, high = array.length - 1;
while (low <= high) {
int mid = (high + low) / 2;
if(array[mid] > k) {
high = mid - 1;
} else {
low = mid + 1;
}
}
if(high < 0 || array[high] != k)
return -1;
return high;
}
項目地址:https://github.com/JohnnyJYWu/offer-Java
上一篇:算法 | 一週刷完《劍指Offer》 Day2:第17~26題
下一篇:算法 | 一週刷完《劍指Offer》 Day4:第38~50題
希望這篇文章對你有幫助~