開個新坑,準備校招研發崗面試,基本的算法還是要過關的。
寫在前面
- 本系列包含《劍指Offer》66道算法題,預計一週刷完。
- 所有題目均可在牛客網在線編程平臺進行調試。
網址:https://www.nowcoder.com/ta/coding-interviews - 本系列包含題目,解題思路及代碼(Java)。
代碼同步發佈在GitHub:https://github.com/JohnnyJYWu/offer-Java - 下一篇:算法 | 一週刷完《劍指Offer》 Day2:第17~26題
Day1:第1~16題
後面總會遇到難的繞的題,前兩天多做幾道,總不會虧的。
- T1. 二維數組中的查找
- T2. 替換空格
- T3. 從尾到頭打印鏈表
- T4. 重建二叉樹
- T5. 用兩個棧實現隊列
- T6. 旋轉數組的最小數字
- T7. 斐波那契數列
- T8. 跳臺階
- T9. 變態跳臺階
- T10. 矩陣覆蓋
- T11. 二進制中 1 的個數
- T12. 數值的整數次方
- T13. 調整數組順序使奇數位於偶數前面
- T14. 鏈表中倒數第 K 個結點
- T15. 反轉鏈表
- T16. 合併兩個排序的鏈表
T1. 二維數組中的查找
題目描述
在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
解題思路
因爲矩陣中的每一個數,左邊都比它小,下邊都比它大。因此,從右上角開始查找,就可以根據 target 和當前元素的大小關係來縮小查找區間。
public boolean Find(int target, int[][] array) {
if(array == null || array.length == 0 || array[0].length == 0) {
return false;
}
int m = 0, n = array[0].length - 1;//從右上角開始找,array[0][n-1]
while(m <= array.length - 1 && n >= 0) {
if(target == array[m][n])
return true;
else if(target > array[m][n])
m ++;
else
n --;
}
return false;
}
T2. 替換空格
題目描述
請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串爲We Are Happy,則經過替換之後的字符串爲We%20Are%20Happy。
解題思路
由於每個空格替換成了三個字符,首先要擴容字符串長度至替換後的長度,因此當遍歷到一個空格時,需要在尾部填充兩個任意字符。
然後聲明兩個下標,一個爲原字符串末尾下標 i,一個爲現字符串末尾 j,兩個下標同步從後往前掃。當 i 指向空格時,j 就倒着依次添加 ‘0’, ‘2’, ‘%’。
這樣做的目的是: j 下標不會超過 i 下標,不會影響原字符串的內容。
public String replaceSpace(StringBuffer str) {
int oldLen = str.length();
for(int i = 0; i < oldLen; i ++) {
if(str.charAt(i) == ' ') {//每出現一個空格,在結尾添加兩個任意字符,擴充字符串長度
str.append("12");
}
}
int i = oldLen - 1;
int j = str.length() - 1;
while(i >= 0 && j > i) {
char c = str.charAt(i);
i --;
if(c == ' ') {//每出現一個空格,替換爲%20
str.setCharAt(j, '0');
j --;
str.setCharAt(j, '2');
j --;
str.setCharAt(j, '%');
j --;
} else {//否則照舊
str.setCharAt(j, c);
j --;
}
}
return str.toString();
}
T3. 從尾到頭打印鏈表
題目描述
輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。
解題思路
使用棧實現:先入後出。全部入棧,依次出棧,順序即爲從尾到頭。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//使用棧實現,先入後出
Stack<Integer> stack = new Stack<>();
ArrayList<Integer> arrayList = new ArrayList<>();
while(listNode != null) {
stack.push(listNode.val);
listNode = listNode.next;
}
while(!stack.isEmpty()) {
arrayList.add(stack.pop());
}
return arrayList;
}
使用遞歸實現:先加入鏈表後面的節點,再加入當前節點,順序即爲從尾到頭。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
//使用遞歸實現,先加入鏈表後面的節點,再加入當前節點
ArrayList<Integer> arrayList = new ArrayList<>();
if(listNode != null) {
arrayList.addAll(printListFromTailToHead(listNode.next));//先遞歸
arrayList.add(listNode.val);//再加入當前
}
return arrayList;
}
T4. 重建二叉樹
題目描述
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。
解題思路
首先清楚以下性質:
前序遍歷:根 -> 左 -> 右,中序遍歷:左 -> 根 -> 右。
因此一顆二叉樹中,前序遍歷的第一個節點爲根節點,通過它在中序遍歷中的位置,可以將中序遍歷分爲兩部分,左半部分是該節點的左子樹,右半部分是該節點的右子樹。
根據這個原理,遞歸執行即可重構出二叉樹。
private Map<Integer, Integer> map = new HashMap<>();//用於查找中序遍歷數組中,每個值對應的索引
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
for(int i = 0; i < in.length; i ++) {
map.put(in[i], i);//key: in的值,value: 值在in中位置
}
return getBiTree(pre, 0, pre.length - 1,
in, 0, in.length - 1);
}
private TreeNode getBiTree(int[] pre, int preLeft, int preRight, //前序遍歷及當前在前序遍歷中的區間
int[] in, int inLeft, int inRight) { //中序遍歷及當前在前序遍歷中的區間
if(preLeft == preRight) { //即根據前序遍歷,當前節點無子節點
return new TreeNode(pre[preLeft]);
}
if(preLeft > preRight || inLeft > inRight) {
return null;
}
TreeNode root = new TreeNode(pre[preLeft]);
int inIndex = map.get(root.val);
int leftTreeSize = inIndex - inLeft;//該節點左半部分的節點數
//遞歸,獲取左子樹及右子樹
root.left = getBiTree(pre, preLeft + 1, preLeft + leftTreeSize, //左半部分在前序遍歷中的區間
in, inLeft, inIndex - 1);//左半部分在中序遍歷中的區間
root.right = getBiTree(pre, preLeft + 1 + leftTreeSize, preRight, //右半部分在前序遍歷中的區間
in, inIndex + 1, inRight);//右半部分在中序遍歷中的區間
return root;
}
T5. 用兩個棧實現隊列
題目描述
用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。
解題思路
隊列:先進先出,棧:先進後出
例如,假設 1 ~ n 的數字順序,入Stack1,出的順序爲 n ~ 1 。
這時,Stack1出棧進入Stack2,進入Stack2的順序爲n ~ 1,那麼Stack2出的順序爲 1 ~ n 。
相當於隊列 1 ~ n 進, 1 ~ n 出。
Stack<Integer> stack1 = new Stack<Integer>();//stack1入棧時使用
Stack<Integer> stack2 = new Stack<Integer>();//stack2出棧時使用,直接出棧即可
public void push(int node) {
while(!stack2.isEmpty()) {//先將stack2中所有元素壓入stack1
stack1.push(stack2.pop());
}
stack1.push(node);//然後將當前要進入隊列元素壓入stack1
while(!stack1.isEmpty()) {//最後將stack1所有元素壓入stack2
stack2.push(stack1.pop());//此時stack2中出棧順序即爲隊列出棧順序,相當於先入先出
}
}
public int pop() {
return stack2.pop();
}
T6. 旋轉數組的最小數字
題目描述
把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。 NOTE:給出的所有元素都大於0,若數組大小爲0,請返回0。
解題思路
我們可以認爲數組分成了兩部分,前半部分是較大的數,後半部分是較小的數。
利用二分查找的思路,觀察取中的數在前半部分還是後半部分,以此來找出最小的數(後半部分的第一個數)。
public int minNumberInRotateArray(int[] array) {
if(array.length == 0) {
return 0;
}
//二分查找
int low = 0, high = array.length - 1;
while (array[low] >= array[high]) {
if(high - low == 1) {
return array[high];
}
int mid = low + (high - low) / 2;//取中
if(array[mid] >= array[low]) {//此時mid在前半區大的數裏
low = mid;
} else {//此時mid在後半區小的數裏
high = mid;
}
}
return array[low];
}
T7. 斐波那契數列
題目描述
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。n<=39
解題思路
斐波那契數列:F[n]=F[n-1]+F[n-2] (n>=3,F[1]=1,F[2]=1)
注意:此題要求F[1]=0。
public int Fibonacci(int n) {//n<=39
int[] array = new int[40];
array[0] = 0;
array[1] = 1;
for(int i = 2; i < array.length; i ++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
T8. 跳臺階
題目描述
一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。
解題思路
動態規劃的思想。這裏採用倒着往前推的方法遞減target,最後一次跳1或最後一次跳2,倒着往前遞歸。
public int JumpFloor(int target) {
if(target == 0) {
return 0;
}
if(target == 1) {
return 1;
}
if(target == 2) {
return 2;
}
if(target > 2) {//遞歸求可能出現的情況總數(最後一次跳1或最後一次跳2,倒着往前推)
return JumpFloor(target - 1) + JumpFloor(target - 2);
}
return 0;
}
或者,其本質上是斐波那契數列的原理。
public int JumpFloor(int target) {
if(target == 0) {
return 0;
}
if(target == 1) {
return 1;
}
int[] array = new int[target];
array[0] = 1;
array[1] = 2;
for (int i = 2; i < target; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[target - 1];
}
T9. 變態跳臺階
題目描述
一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
解題思路
同理,動態規劃的思想,遞歸求解。(這次情況有點多,用到循環了)
public int JumpFloorII(int target) {
if(target == 0 || target == 1) {
return 1;
}
if(target == 2) {
return 2;
}
int sum = 0;
for(int i = 0; i < target; i ++) {//本次跳0次到跳target-1次
sum += JumpFloorII(i);//對於本次的跳躍又有多少種跳法?遞歸獲取結果
}
return sum;
}
T10. 矩陣覆蓋
題目描述
我們可以用 2 * 1 的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個 2 * 1 的小矩形無重疊地覆蓋一個 2 * n 的大矩形,總共有多少種方法?
解題思路
原理同T8,詳見圖片。
public int RectCover(int target) {
if(target == 0) {
return 0;
}
if(target == 1) {
return 1;
}
int[] array = new int[target];
array[0] = 1;
array[1] = 2;
for (int i = 2; i < target; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[target - 1];
}
T11. 二進制中 1 的個數
題目描述
輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。
解題思路
Integer.bitCount(n):技術整型的二進制表示中1的個數。。。
public int NumberOf1(int n) {//(???)
return Integer.bitCount(n);
}
正常算法:&按位與。每次將 n 和 n-1 進行 & 運算,從右往左去掉二進制最右邊的一個1。例如:
n: 100100
n - 1: 100011
n & (n-1): 100000
一次運算後,n由100100變爲100000,去除了一個1。所有1去完時,n爲0。
public int NumberOf1(int n) {//正常算法
int sum = 0;
while(n != 0) {
sum ++;
n = n & (n-1);//&按位與
}
return sum;
}
T12. 數值的整數次方
題目描述
給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。
解題思路
由於xn = (x*x)n/2,通過遞歸求解可減小時間複雜度,每遞歸一次,n 減一半。時間複雜度O(logn)。
注意:需要小心 n 的正負、奇偶。
public double Power(double base, int exponent) {
if(exponent == 0) return 1;
if(exponent == 1) return base;
boolean isPositive = true;//正負次方以此判斷
if(exponent < 0) {
exponent = -exponent;
isPositive = false;
}
double pow = Power(base * base, exponent / 2);//遞歸,每次遞歸n減小一半,即 (x*x)^(n/2)
if(exponent % 2 != 0) pow *= base;//奇次冪的話,/2會少算一次,在這補回來
return isPositive ? pow : (1 / pow);//三元表達式,正次冪爲pow,負次冪爲1/pow
}
T13. 調整數組順序使奇數位於偶數前面
題目描述
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。
解題思路
先統計奇偶數的個數。在所要求的數組中,奇數的個數即爲數組中偶數應該開始儲存的位置。
clone一個數組,按順序往原數組裏存,奇數存前面,偶數存後面。
public void reOrderArray(int[] array) {
int oddNum = 0;
for(int value: array) {
if(value % 2 == 1) {
oddNum++;
}
}
int[] copyArray = array.clone();//克隆數組,對原數組賦值
int i = 0, j = oddNum;//j爲偶數開始存儲的位置
for(int num : copyArray) {
if(num % 2 == 1) {
array[i] = num;
i ++;
} else {
array[j++] = num;
j ++;
}
}
}
或者,聲明兩個ArrayList存奇數和偶數,然後合併。
public void reOrderArray(int[] array) {
ArrayList<Integer> odd = new ArrayList<>();
ArrayList<Integer> even = new ArrayList<>();
for(int i = 0; i < array.length; i ++) {
if(array[i] % 2 == 0) {
even.add(array[i]);
} else {
odd.add(array[i]);
}
}
odd.addAll(even);
for(int i = 0; i < array.length; i ++) {
array[i] = odd.get(i);
}
}
T14. 鏈表中倒數第 K 個結點
題目描述
輸入一個鏈表,輸出該鏈表中倒數第k個結點。
解題思路
假設,共 n 個節點,倒數第k個結點即爲第 n-k+1 個節點。
定義兩個指針node1和node2,使他們都指向第一個節點。到第 n-k+1 個節點則需要移動 n-k次。
此時,node1移動n次會指向空。
先讓node1移動 k 次,還剩 n-k 次指向空。
這時,node2與node1同步移動,當node1指空時,node2移動了 n-k 次,剛好到第 n-k+1 個節點。
public ListNode FindKthToTail(ListNode head, int k) {
if(head == null) return null;
ListNode node1 = head;
ListNode node2 = head;
while(node1 != null && k > 0) {//node1移動k次,還有n-k次會指空
node1 = node1.next;
k --;
}
if(k > 0) return null;
while(node1 != null) {//移動n-k次,此時node2到n-k+1個節點,即倒數第k個節點
node1 = node1.next;
node2 = node2.next;
}
return node2;
}
T15. 反轉鏈表
題目描述
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。
解題思路
關鍵在於聲明一個節點preNode用來記錄前一個節點,使下一次循環時的節點的next指向它,反轉順序。
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode preNode = null;
while(head != null) {
ListNode tmp = head.next;//tmp記錄【下一個節點】
head.next = preNode;//【當前節點】的next指向【前一個節點】,反轉鏈表順序
preNode = head;//preNode記錄【當前節點】,即【下一個節點】的【前一個節點】
head = tmp;//將【當前節點】更改爲【下一個節點】,進入下一次循環
}
//當head爲null時跳出了循環,此時它的前一個節點preNode即爲原鏈表的最後一個節點,鏈表順序已反轉
return preNode;
}
T16. 合併兩個排序的鏈表
題目描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。
解題思路
聲明一個新鏈表,不斷比較原來兩個鏈表的 val 值,小的插入新鏈表即可。
注意:要求返回的表頭是聲明的鏈表的next節點。
public ListNode Merge(ListNode list1, ListNode list2) {
ListNode head = new ListNode(-9999);
ListNode tmp = head;
while(list1 != null && list2 != null) {
if(list1.val < list2.val) {//比較兩個鏈表當前節點的值,小的先插入新鏈表
tmp.next = list1;
list1 = list1.next;
} else {
tmp.next = list2;
list2 = list2.next;
}
tmp = tmp.next;
}
//容錯,將剩餘鏈表補到新鏈表結尾,此時能保持單調不減
if(list1 != null) tmp.next = list1;
if(list2 != null) tmp.next = list2;
return head.next;//記得head爲聲明的無意義表頭,head.next纔是新鏈表的頭
}
項目地址:https://github.com/JohnnyJYWu/offer-Java
下一篇:算法 | 一週刷完《劍指Offer》 Day2:第17~26題
希望這篇文章對你有幫助~