算法難,難如上青天,可是難也得靜下心來慢慢學習,並總結概括。因此將劍指 offer
中的題目按照類別進行了概括,這是第一篇--數組篇。固然,若是各位大佬發現程序有什麼 bug
或其餘更巧妙的思路,歡迎交流學習。java
題目一描述算法
在一個長度爲 n 的數組裏的全部數字都在 0~n-1 的範圍內。數組中存在有重複的數字,但不知道有幾個數字重複,也不知道重複了幾回。請找出數組中任意一個重複的數字。數組
解題思路函數
因爲數組中全部數字都在 0 ~ n-1
範圍內,那麼若是數組中沒有重複的數字,則排序後的數組中,數字 i
就必定出如今下標爲 i
的位置。學習
因此,能夠在遍歷數組的時候,判斷:spa
arr[i]
等於 i
,則繼續遍歷;arr[i]
與 arr[arr[i]]
進行比較:
arr[i]
放到下標爲 i
的位置。而後繼續重複步驟 2
進行比較。代碼實現指針
public boolean duplicate(int[] arr, int[] duplication) {
if (arr == null || arr.length <= 0) {
return false;
}
for (int i = 0; i < arr.length; i++) {
while (arr[i] != i) {
if (arr[i] == arr[arr[i]]) {
duplication[0] = arr[i];
return true;
}
int temp = arr[i];
arr[i] = arr[temp];
arr[temp] = temp;
}
}
return false;
}
複製代碼
這種方法,每個數字最多隻須要交換兩次就能夠歸位:code
所以時間複雜度是 O(n)
。因爲不須要額外空間,空間複雜度是 O(1)
。排序
題目二描述遞歸
在一個長度爲 n+1 的數組中全部數字都在 1~n 範圍內,因此數組中至少有一個數字重複。請找出任意一個重複的數字,可是不能修改原有的數組。
解題思路
因爲數組中全部數字都在 1 ~ n
範圍內,因此能夠將 1 ~ n
的數組從中間值 m
分爲 1 ~ m
和 m+1 ~ n
兩部分。若是 1 ~ m
之間的數字超過了 m
個,表示重複數字在 1 ~ m
之間,不然在 m+1 ~ n
之間。
而後繼續將包含重複數字的區間分爲兩部分,繼續判斷直到找到一個重複的數字。
代碼實現
public int duplicateNumber(int[] arr) {
if (arr == null || arr.length <= 0) {
return -1;
}
int start = 1;
int end = arr.length - 1;
while (start <= end) {
int mid = ((end - start) >> 1) + start;
int count = countRange(arr, start, mid);
if (start == mid) {
if (count > 1) {
return start;
} else {
break;
}
}
if (count > (mid - start + 1)) {
end = mid;
} else {
start = mid + 1;
}
}
return -1;
}
public int countRange(int[] arr, int start, int end) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] >= start && arr[i] <= end) {
count++;
}
}
return count;
}
複製代碼
按照二分查找的思路,函數 countRange
會被調用 log(n)
次,每次須要 O(n)
的時間,因此總的時間複雜度是 O(nlogn)
。
題目描述
在一個二維數組中,每一行按照從左到右遞增的順序排序,每一列按照從上到下遞增的順序排序。要求實現一個函數,輸入一個二位數組和一個整數,判斷該整數是否在數組中。
解題思路
這裏能夠選取左下角或右上角的元素進行比較。這裏,以右上角爲例:
對於右上角的元素,若是該元素大於要查找的數字,則要查找的數字必定在它的左邊,將 col--
,若是該元素小於要查找的數字,則要查找的數字必定在它的下邊,將 row++
,不然,找到了該元素,查找結束。
public boolean find(int target, int [][] array) {
if(array == null || array.length <= 0 || array[0].length <= 0) {
return false;
}
int row = 0;
int col = array[0].length - 1;
while(row < array.length && col >= 0){
if(array[row][col] > target) {
col--;
} else if(array[row][col] < target) {
row++;
} else {
return true;
}
}
return false;
}
複製代碼
題目描述
將一個數組最開始的幾個元素移動數組的末尾,稱爲旋轉數組。輸入一個遞增排序的數組的一個旋轉,輸出旋轉數組的最小元素。
解題思路
因爲數組在必定程度上是有序的,因此能夠採用相似二分查找的方法來解決。可使用兩個指針,start 指向數組的第一個元素,end 指向最後一個元素,接着讓 mid 指向數組的中間元素。
這裏須要考慮一類特殊狀況,就是數組中存在重複元素,例如 1 1 1 0 1
或者 1 0 1 1 1
的狀況,這時利用二分法已經不能解決,只能進行順序遍歷。
通常狀況下,判斷數組中間元素(mid
)與數組最後一個元素(end
)的大小,若是數組中間元素大於最後一個元素,則中間元素屬於前半部分的非遞減子數組,例如 3 4 5 1 2
。此時最小的元素必定位於中間元素的後面,則將 start
變爲 mid + 1
。
不然的話,也就是數組中間元素(mid
)小於等於最後一個元素(end
),則中間元素屬於後半部分的非遞減子數組中,例如 2 0 1 1 1
,或者 4 5 1 2 3
。此時最小的元素可能就是中間元素,可能在中間元素的前面,因此將 end
變爲 mid
。
如此,直到 start
大於等於 end
退出循環時,start
指向的就是最小的元素。
代碼實現
public int minNumberInRotateArray(int [] array) {
if(array == null || array.length <= 0) {
return 0;
}
int start = 0;
int end = array.length - 1;
// 數組長度爲 1 時,該元素必然是最小的元素,也就不須要再判斷 start == end 的狀況
while(start < end) {
int mid = start + ((end - start) >> 1);
if (array[start] == array[end] && array[start] == array[mid]) {
return min(array, start, end);
}
if(array[mid] > array[end]) {
start = mid + 1;
} else {
end = mid;
}
}
return array[start];
}
public int min(int[] array, int start, int end) {
int min = array[start];
for(int i = start + 1; i <= end; i++) {
if(array[i] < min) {
min = array[i];
}
}
return min;
}
複製代碼
題目描述
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得全部的奇數位於數組的前半部分,全部的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。
解題思路
這裏有兩種解題思路:第一種是利用插入排序的思路(其實只要能保證穩定性的排序算法均可以),遍歷數組,若是該元素是奇數,則對前面的元素進行,若是前面的元素是偶數則進行交換,直到找到一個奇數爲止。
第二種是藉助輔助數組,首先遍歷一遍數組,將全部奇數元素保存到輔助數組中,並計算出奇數元素的個數;而後再遍歷一遍輔助數組,將其中全部奇數元素放到原數組的前半部分,將全部偶數元素放到從 count
開始的後半部分。
代碼實現
// 時間複雜度 O(n^2)
public static void reorderOddEven1(int[] data) {
for (int i = 1; i < data.length; i++) {
if ((data[i] & 1) == 1) {
int temp = data[i];
int j = i - 1;
for (; j >= 0 && (data[j] & 1) == 0; j--) {
data[j + 1] = data[j];
}
data[j + 1] = temp;
}
}
}
// 時間複雜度 O(n) 空間複雜度 O(n)
public static void reorderOddEven2(int[] data) {
if (data == null || data.length <= 0) {
return;
}
int count = 0;
int[] tempArr = new int[data.length];
for (int i = 0; i < data.length; i++) {
if ((data[i] & 1) == 1) {
count++;
}
tempArr[i] = data[i];
}
int j = 0, k = count;
for (int i = 0; i < data.length; i++) {
if ((tempArr[i] & 1) == 1) {
data[j++] = tempArr[i];
} else {
data[k++] = tempArr[i];
}
}
}
複製代碼
這裏第一種作法,和插入排序的時間複雜度一致,平均狀況下時間複雜度爲 O(n^2)
,在最好狀況下時間複雜度是 O(n)
。
而第二種作法,因爲只須要遍歷兩次數組,因此時間複雜度爲 O(n)
。可是須要藉助輔助數組,因此空間複雜度是 O(n)
。
題目描述
輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每個數字。若是輸入以下 4 X 4 矩陣:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
則依次打印出數字 1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10。
解題思路
在打印矩陣時,能夠按照從外到內一圈一圈來打印,因而可使用循環來打印矩陣,每次循環打印一圈。對於一個 5 * 5
的矩陣,循環結束條件是 2 * 2 < 5
,而對於一個 6 * 6
的矩陣,循環結束條件是 2 * 3 < 6
。因此能夠得出循環結束的條件是 2 * start < rows && 2 * start < cols
。
在打印一圈時,能夠分爲從左到右打印第一行、從上到下打印最後一列、從右到左打印最後一行、從下到上打印第一列。可是這裏須要考慮最後一圈退化爲一行、一列的狀況。
代碼實現
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res = new ArrayList<>();
int rows, cols;
if(matrix == null || (rows = matrix.length) <= 0 || (cols = matrix[0].length) <= 0){
return res;
}
int i = 0;
while(2 * i < rows && 2 * i < cols) {
printMatrixCore(matrix, i++, res);
}
return res;
}
public void printMatrixCore(int[][] matrix, int start, ArrayList<Integer> res) {
int endX = matrix.length - start - 1;
int endY = matrix[0].length - start - 1;
// 第一行老是存在的
for(int i = start; i <= endY; i++) {
res.add(matrix[start][i]);
}
// 至少要有兩行
if(endX > start) {
for(int j = start + 1; j <= endX; j++) {
res.add(matrix[j][endY]);
}
}
// 至少要有兩行兩列
if(endX > start && endY > start) {
for(int i = endY - 1; i >= start; i--) {
res.add(matrix[endX][i]);
}
}
// 至少要有三行兩列
if(endX > start + 1 && endY > start) {
for(int j = endX - 1; j > start; j--) {
res.add(matrix[j][start]);
}
}
}
複製代碼
題目描述
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。例如輸入一個長度爲 9 的數組 {1,2,3,2,2,2,5,4,2}。因爲數字 2 在數組中出現了 5 次,超過數組長度的一半,所以輸出 2。若是不存在則輸出 0。
解題思路1
因爲有一個數字出現次數超過了數組長度的一半,因此若是數組有序的話,那麼數組的中位數必然是出現次數超過一半的數。
可是這裏沒有必要徹底對數組排好序。能夠利用快速排序的思想,使用 partition
函數,對數組進行切分,使得切分元素以前的元素都小於等於它,以後的元素都大於等於它。
一次切分以後能夠將切分元素的下標 index
與數組中間的 mid
比較,若是 index
大於 mid
,表示中間值在左半部分,將 end = mid - 1
,繼續進行切分;而若是 index
小於 mid
,表示中間值在右半部分,將 start = mid + 1
,繼續進行切分;不然表示找到了出現次數超過一半的元素。
代碼實現1
public int MoreThanHalfNum_Solution(int [] array) {
if(array == null || array.length <= 0) {
return 0;
}
int start = 0;
int end = array.length - 1;
int mid = (end - start) >> 1;
int index = partition(array, start, end);
while(index != mid) {
if(index > mid) {
end = index - 1;
index = partition(array, start, end);
} else {
start = index + 1;
index = partition(array, start, end);
}
}
if(checkMoreThanHalf(array, array[index])) {
return array[index];
}
return 0;
}
public int partition(int[] array, int left, int right) {
int pivot = array[left];
int i = left, j = right + 1;
while(true) {
while(i < right && array[++i] < pivot) {
if(i == right) { break; }
}
while(j > left && array[--j] > pivot) { }
if(i >= j) {
break;
}
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
array[left] = array[j];
array[j] = pivot;
return j;
}
public boolean checkMoreThanHalf(int[] array, int res) {
int count = 0;
for(int i = 0; i < array.length; i++) {
if(array[i] == res) {
count++;
}
}
return count * 2 > array.length;
}
複製代碼
解題思路2
還有一種解題思路,它是利用數組的特色,使用一個 times
來記錄某個數的出現的次數,而後遍歷數組,若是 times
爲 0
,將當前元素賦給 result
,並將 times
置爲 1
;不然若是當前元素等於 result
,則將 times
加 1
,不然將 times
減 1
。
如此在遍歷完數組,出現次數 times
大於等於 1
對應的那個數必定就是出現次數超過數組一半長度的數。
代碼實現2
public static int moreThanHalfNum(int[] number) {
if (number == null || number.length <= 0) {
return -1;
}
int result = 0;
int times = 0;
for (int i = 0; i < number.length; i++) {
if (times == 0) {
result = number[i];
times = 1;
} else if (result == number[i]) {
times++;
} else {
times--;
}
}
if (checkMoreThanHalf(number, result)) {
return result;
}
return -1;
}
private static boolean checkMoreThanHalf(int[] number, int result) {
int count = 0;
for (int a : number) {
if (a == result) {
count++;
}
}
return count * 2 > number.length;
}
複製代碼
這種方法只須要遍歷一遍數組,就能夠找到找到數組中出現次數超過一半的數,因此時間複雜度是 O(n)
。雖然與前一種方法的時間複雜度一致,但無疑簡潔了很多。
題目描述
輸入一個整型數組。數組裏有正數和負數。數組中一個或多個連續的整數組成一個子數組,求全部子數組和的最大值。
解題思路
能夠從頭至尾遍歷數組,若是前面數個元素之和 lastSum
小於 0
,就將其捨棄,將 curSum
賦值爲 array[i]
。不然將前面數個元素之和 lastSum
加上當前元素 array[i]
,獲得新的和 curSum
。而後判斷這個和 curSum
與保存的最大和 maxSum
,若是 curSum
大於 maxSum
,則將其替換。而後更新 lastSum
,繼續遍歷數組進行比較。
代碼實現
public int findGreatestSumOfSubArray(int[] array) {
if(array == null || array.length <= 0) {
return -1;
}
int lastSum = 0;
int curSum = 0;
int maxSum = Integer.MIN_VALUE;
for(int i = 0; i < array.length; i++) {
if(lastSum <= 0) {
curSum = array[i];
} else {
curSum = lastSum + array[i];
}
if(curSum > maxSum) {
maxSum = curSum;
}
lastSum = curSum;
}
return maxSum;
}
複製代碼
題目描述
在數組中的兩個數字,若是前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數 P,並將 P 對 1000000007 取模的結果輸出,即輸出 P%1000000007。
解題思路
首先把數組分紅兩個子數組,而後遞歸地對子數組求逆序對,統計出子數組內部的逆序對的數目。
因爲已經統計了子數組內部的逆序對的數目,因此須要這兩個子數組進行排序,避免在後面重復統計。在排序的時候,還要統計兩個子數組之間的逆序對的數目。
注意,這裏若是 aux[i] > aux[j]
,應該是 count += mid + 1 - i;
,也就是從下標爲 i ~ mid
的元素與下標爲 j
的元素都構成了逆序對。而若是是 count += j - mid;
的話,則成了下標爲 i
的元素與下標爲 mid + 1 ~ j
的元素構成了逆序對,後面會出現重複統計的狀況。
最後對兩個子數組內部的逆序對和兩個子數組之間的逆序對相加,返回便可。
代碼實現
public int inversePairs(int [] array) {
if(array == null || array.length <= 0) {
return 0;
}
int[] aux = new int[array.length];
return inversePairs(array, aux, 0, array.length - 1);
}
public int inversePairs(int[] data, int[] aux, int start, int end) {
if(start >= end) {
return 0;
}
int mid = start + ((end - start) >> 1);
int left = inversePairs(data, aux, start, mid);
int right = inversePairs(data, aux, mid + 1, end);
for(int i = start; i <= end; i++) {
aux[i] = data[i];
}
int i = start;
int j = mid + 1;
int count = 0;
for(int k = start; k <= end; k++) {
if(i > mid) {
data[k] = aux[j++];
} else if(j > end) {
data[k] = aux[i++];
} else if(aux[i] > aux[j]) {
data[k] = aux[j++];
count += mid + 1 - i;
count %= 1000000007;
} else {
data[k] = aux[i++];
}
}
return (left + right + count) % 1000000007;
}
複製代碼
這種方法與歸併排序的時間、空間複雜度一致,每次排序的時間爲 O(n)
,總共須要 O(logn)
次,因此總的時間複雜度是 O(nlogn)
。在歸併時須要輔助數組,因此其空間複雜度爲 O(n)
。
題目一描述
統計一個數字在排序數組中出現的次數。
解題思路
對於排序數組,可使用兩次二分查找分別找到要查找的數字第一次和最後一次出現的數組下標。而後就能夠計算出該數字出現的次數。
查找第一次出現的數組下標時,若是數組中間元素大於該數字 k
,則在數組左半部分去查找,不然數組中間元素小於該數字 k
,則在數組右半部分去查找。
當中間元素等於 k
時,則須要判斷 mid
,若是 mid
前面沒有數字,或者前面的數字不等於 k
,則找到了第一次出現的數組下標;不然繼續在數組左半部分去查找。
查找最後一次出現的數組下標與查找第一次出現的思想相似,這裏就再也不贅述了。
代碼實現
public int GetNumberOfK(int [] array , int k) {
if(array == null || array.length <= 0) {
return 0;
}
int left = getFirstIndex(array, k);
int right = getLastIndex(array, k);
if(left != -1 && right != -1) {
return right - left + 1;
}
return 0;
}
public int getFirstIndex(int[] array, int k) {
int start = 0;
int end = array.length - 1;
while(start <= end) {
int mid = start + ((end - start) >> 1);
if(k == array[mid]) {
if(mid == 0 || array[mid - 1] != k) {
return mid;
} else {
end = mid - 1;
}
} else if(k < array[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
}
return -1;
}
public int getLastIndex(int[] array, int k) {
int start = 0;
int end = array.length - 1;
while(start <= end) {
int mid = start + ((end - start) >> 1);
if(k == array[mid]) {
if(mid == end || array[mid + 1] != k) {
return mid;
} else {
start = mid + 1;
}
} else if(k < array[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
}
return -1;
}
複製代碼
題目二描述
一個長度爲 n 的遞增數組中的全部數字都是惟一的,而且每一個數字都在 [0, n] 範圍內,在 [0, n] 範圍內的 n+1 個數字中有且只有一個數字不在數組中,請找出這個數字。
解題思路
因爲數組是有序的,因此數組開始的一部分數字與它們對應的下標是相等。若是不在數組中的數字爲 m
,則它前面的數字與它們的下標都相等,它後面的數字比它們的下標都要小。
可使用二分查找,若是中間元素的值和下標相等,則在數組右半部分查找;若是不相等,則須要進一步判斷,若是它前面沒有元素,或者前面的數字和它的下標相等,則找到了 m
;不然繼續在左半部分查找。
代碼實現
public static int getMissingNumber(int[] arr) {
if (arr == null || arr.length <= 0) {
return -1;
}
int start = 0;
int end = arr.length - 1;
while (start <= end) {
int mid = start + ((end - start) >> 1);
if (arr[mid] != mid) {
// 當前不相等,前一個相等,表示找到了
if (mid == 0 || arr[mid - 1] == mid - 1) {
return mid;
// 左半邊查找
} else {
end = mid - 1;
}
} else {
//右半邊查找
start = mid + 1;
}
}
if (start == arr.length) {
return arr.length;
}
return -1;
}
複製代碼
題目一描述
一個整型數組裏除了兩個數字以外,其餘的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。
解題思路
這裏解題思路有些巧妙,使用位元素來解決,因爲兩個相等的數字異或後結果爲 0
,因此遍歷該數組,依次異或數組中的每個元素,那麼最終的結果就是那兩個只出現一次的數字異或的結果。
因爲這兩個數字確定不同,異或的結果也確定不爲 0
,也就是它的二進制表示中至少有一位是 1
,將該位求出後記爲 n
。
能夠將以第 n
位爲標準將原數組分爲兩個數組,第一個數組中第 n
位是 1
,而第二個數組中第 n
位是 0
,而兩個只出現一次的數字必然各出如今一個數組中,而且數組中的元素異或的結果就是隻出現一次的那個數字。
代碼實現
public void findNumsAppearOnce(int [] array,int num1[] , int num2[]) {
if(array == null || array.length <= 0) {
return;
}
int num = 0;
for(int i = 0; i < array.length; i++) {
num ^= array[i];
}
int index = bitOf1(num);
int mark = 1 << index;
for(int i = 0; i < array.length; i++) {
if((array[i] & mark) == 0) {
num1[0] ^= array[i];
} else {
num2[0] ^= array[i];
}
}
}
public int bitOf1(int num) {
int count = 0;
while((num & 1) == 0) {
num >>= 1;
count++;
}
return count;
}
複製代碼
題目二描述
一個整型數組裏除了一個數字只出現了一次以外,其餘的數字都出現了三次。請找出那個只出現一次的數字。
解題思路
這裏因爲出現了三次,雖然不能再使用異或運算,但一樣可使用位運算。能夠從頭至尾遍歷數組,將數組中每個元素的二進制表示的每一位都加起來,使用一個 32
位的輔助數組來存儲二進制表示的每一位的和。
對於全部出現三次的元素,它們的二進制表示的每一位之和,確定能夠被 3
整除,因此最終輔助數組中若是某一位能被 3
整除,那麼那個只出現一次的整數的二進制表示的那一位就是 0
,不然就是 1
。
代碼實現
public static void findNumberAppearOnce(int[] arr, int[] num) {
if (arr == null || arr.length < 2) {
return;
}
int[] bitSum = new int[32];
for (int i = 0; i < arr.length; i++) {
int bitMask = 1;
for (int j = 31; j >= 0; j--) {
int bit = arr[i] & bitMask;
if (bit != 0) {
bitSum[j]++;
}
bitMask <<= 1;
}
}
int result = 0;
for (int i = 0; i < 32; i++) {
result <<= 1;
result += bitSum[i] % 3;
}
num[0] = result;
}
複製代碼
題目描述
給定一個數組 A[0,1,...,n-1],請構建一個數組 B[0,1,...,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×...×A[i-1]×A[i+1]×...×A[n-1]。不能使用除法。
解題思路
這裏要求 B[i] = A[0] * A[1] * ... * A[i-1] * A[i+1] * ... * A[n-1]
,能夠將其分爲分爲兩個部分的乘積。
A[0] * A[1] * ... * A[i-2] * A[i-1]
A[i+1] * A[i+2] * ... * A[n-2] * A[n-1]
複製代碼
可使用兩個循環,第一個循環採用自上而下的順序,res[i] = res[i - 1] * arr[i - 1]
計算前半部分,第二循環採用自下而上的順序,res[i] *= (temp *= arr[i + 1])
。
代碼實現
public int[] multiply(int[] arr) {
// arr [2, 1, 3, 4, 5]
if (arr == null || arr.length <= 0) {
return null;
}
int[] result = new int[arr.length];
result[0] = 1;
for (int i = 1; i < arr.length; i++) {
result[i] = arr[i - 1] * result[i - 1];
}
// result [1, 2, 2, 6, 24]
int temp = 1;
for (int i = result.length - 2; i >= 0; i--) {
temp = arr[i + 1] * temp;
result[i] = result[i] * temp;
}
// temp 60 60 20 5 1
// result [60, 120, 40, 30, 24]
return result;
}
複製代碼