文章目錄
- 03_01_DuplicationInArray
- 03_02_DuplicationInArrayNoEdit
- 04_FindInPartiallySortedMatrix
- 05_ReplaceSpaces
- 06_PrintListInReversedOrder
- 07_ConstructBinaryTree
- 08_NextNodeInBinaryTrees
- 09_01_QueueWithTwoStacks
- 09_02_StackWithTwoQueues
- 10_01_Fibonacci
- 10_02_JumpFloor
- 10_03_JumpFloorII
- 10_04_RectCover
- 11_MinNumberInRotatedArray
- 12_StringPathInMatrix
- 13_RobotMove
- 14_CuttingRope
- 15_NumberOf1InBinary
- 16_Power
- 17_Print1ToMaxOfNDigits
- 18_01_DeleteNodeInList
- 18_02_DeleteDuplicatedNode
- 19_RegularExpressionsMatching
- 20_NumericStrings
03_01_DuplicationInArray
找出數組中重複的數字
題目描述
在一個長度爲 n
的數組裏的所有數字都在 0
到 n-1
的範圍內。數組中某些數字是重複的,但不知道有幾個數字重複了,也不知道每個數字重複了幾次。請找出數組中任意一個重複的數字。例如,如果輸入長度爲 7
的數組 {2, 3, 1, 0, 2, 5, 3}
,那麼對應的輸出是重複的數字 2
或者 3
。
解法
解法一
排序後,順序掃描,判斷是否有重複,時間複雜度爲 O(n²)
。
解法二
利用哈希表,遍歷數組,如果哈希表中沒有該元素,則存入哈希表中,否則返回重複的元素。時間複雜度爲 O(n)
,空間複雜度爲 O(n)
。
解法三
長度爲 n
,元素的數值範圍也爲 n
,如果沒有重複元素,那麼數組每個下標對應的值與下標相等。
從頭到尾遍歷數組,當掃描到下標 i
的數字 nums[i]
:
- 如果等於
i
,繼續向下掃描; - 如果不等於
i
,拿它與第nums[i]
個數進行比較,如果相等,說明有重複值,返回nums[i]
。如果不相等,就把第i
個數 和第nums[i]
個數交換。重複這個比較交換的過程。
此算法時間複雜度爲 O(n)
,因爲每個元素最多隻要兩次交換,就能確定位置。空間複雜度爲 O(1)
。
/**
* @author bingo
* @since 2018/10/27
*/
public class Solution {
/**
* 查找數組中的重複元素
* @param numbers 數組
* @param length 數組長度
* @param duplication duplication[0]存儲重複元素
* @return boolean
*/
public boolean duplicate(int[] numbers, int length, int[] duplication) {
if (numbers == null || length < 1) {
return false;
}
for (int e : numbers) {
if (e >= length) {
return false;
}
}
for (int i = 0; i < length; ++i) {
while (numbers[i] != i) {
if (numbers[i] == numbers[numbers[i]]) {
duplication[0] = numbers[i];
return true;
}
swap(numbers, i, numbers[i]);
}
}
return false;
}
private void swap(int[] numbers, int i, int j) {
int t = numbers[i];
numbers[i] = numbers[j];
numbers[j] = t;
}
}
測試用例
- 長度爲 n 的數組中包含一個或多個重複的數字;
- 數組中不包含重複的數字;
- 無效測試輸入用例(輸入空指針;長度爲 n 的數組中包含 0~n-1 之外的數字)。
03_02_DuplicationInArrayNoEdit
不修改數組找出重複的數字
題目描述
在一個長度爲 n+1
的數組裏的所有數字都在 1
到 n
的範圍內,所以數組中至少有一個數字是重複的。請找出數組中任意一個重複的數字,但不能修改輸入的數組。例如,如果輸入長度爲 8
的數組 {2, 3, 5, 4, 3, 2, 6, 7}
,那麼對應的輸出是重複的數字 2
或者 3
。
解法
解法一
創建長度爲 n+1
的輔助數組,把原數組的元素複製到輔助數組中。如果原數組被複制的數是 m
,則放到輔助數組第 m
個位置。這樣很容易找出重複元素。空間複雜度爲 O(n)
。
解法二
數組元素的取值範圍是 [1, n]
,對該範圍對半劃分,分成 [1, middle]
, [middle+1, n]
。計算數組中有多少個(count)元素落在 [1, middle]
區間內,如果 count 大於 middle-1+1,那麼說明這個範圍內有重複元素,否則在另一個範圍內。繼續對這個範圍對半劃分,繼續統計區間內元素數量。
時間複雜度 O(n * log n)
,空間複雜度 O(1)
。
注意,此方法無法找出所有重複的元素。
/**
* @author bingo
* @since 2018/10/27
*/
public class Solution {
/**
* 不修改數組查找重複的元素,沒有則返回-1
* @param numbers 數組
* @return 重複的元素
*/
public int getDuplication(int[] numbers) {
if (numbers == null || numbers.length < 1) {
return -1;
}
int start = 1;
int end = numbers.length - 1;
while (end >= start) {
int middle = start + ((end - start) >> 1);
// 調用 log n 次
int count = countRange(numbers, start, middle);
if (start == end) {
if (count > 1) {
return start;
}
break;
} else {
// 無法找出所有重複的數
if (count > (middle - start) + 1) {
end = middle;
} else {
start = middle + 1;
}
}
}
return -1;
}
/**
* 計算整個數組中有多少個數的取值在[start, end] 之間
* 時間複雜度 O(n)
* @param numbers 數組
* @param start 左邊界
* @param end 右邊界
* @return 數量
*/
private int countRange(int[] numbers, int start, int end) {
if (numbers == null) {
return 0;
}
int count = 0;
for(int e : numbers) {
if (e >= start && e <= end) {
++count;
}
}
return count;
}
}
測試用例
- 長度爲 n 的數組中包含一個或多個重複的數字;
- 數組中不包含重複的數字;
- 無效測試輸入用例(輸入空指針)。
04_FindInPartiallySortedMatrix
二維數組中的查找
題目描述
在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
解法
從二維數組的右上方開始查找:
- 若元素值等於
target
,返回true
; - 若元素值大於
target
,砍掉這一列,即--j
; - 若元素值小於
target
,砍掉這一行,即++i
。
也可以從二維數組的左下方開始查找,以下代碼使用左下方作爲查找的起點。
注意,不能選擇左上方或者右下方的數字,因爲這樣無法縮小查找的範圍。
/**
* @author bingo
* @since 2018/10/27
*/
public class Solution {
/**
* 二維數組中的查找
* @param target 目標值
* @param array 二維數組
* @return boolean
*/
public boolean find(int target, int[][] array) {
if (array == null) {
return false;
}
int rows = array.length;
int columns = array[0].length;
int i = rows - 1;
int j = 0;
while (i >= 0 && j < columns) {
if (array[i][j] == target) {
return true;
}
if (array[i][j] < target) {
++j;
} else {
--i;
}
}
return false;
}
}
測試用例
- 二維數組中包含查找的數字(查找的數字是數組中的最大值和最小值;查找的數字介於數組中的最大值和最小值之間);
- 二維數組中沒有查找的數字(查找的數字大於/小於數組中的最大值;查找的數字在數組的最大值和最小值之間但數組中沒有這個數字);
- 特殊輸入測試(輸入空指針)。
05_ReplaceSpaces
替換空格
題目描述
請實現一個函數,將一個字符串中的每個空格替換成 %20
。例如,當字符串爲 We Are Happy
,則經過替換之後的字符串爲 We%20Are%20Happy
。
解法
解法一
創建 StringBuilder
,遍歷原字符串,遇到非空格,直接 append 到 StringBuilder
中,遇到空格則將 %20
append 到 StringBuilder
中。
/**
* @author bingo
* @since 2018/10/27
*/
public class Solution {
/**
* 將字符串中的所有空格替換爲%20
* @param str 字符串
* @return 替換後的字符串
*/
public String replaceSpace(StringBuffer str) {
if (str == null || str.length() == 0) {
return str.toString();
}
StringBuilder sb = new StringBuilder();
int len = str.length();
for (int i = 0; i < len; ++i) {
char ch = str.charAt(i);
sb.append(ch == ' ' ? "%20" : ch);
}
return sb.toString();
}
}
解法二【推薦】
先遍歷原字符串,遇到空格,則在原字符串末尾 append
任意兩個字符,如兩個空格。
用指針 p
指向原字符串末尾,q
指向現字符串末尾,p
, q
從後往前遍歷,當 p
遇到空格,q
位置依次要 append
‘02%’,若不是空格,直接 append
p
指向的字符。
?思路擴展:
在合併兩個數組(包括字符串)時,如果從前往後複製每個數字(或字符)需要重複移動數字(或字符)多次,那麼我們可以考慮從後往前複製,這樣就能減少移動的次數,從而提高效率。
/**
* @author bingo
* @since 2018/10/27
*/
public class Solution {
/**
* 將字符串中的所有空格替換爲%20
* @param str 字符串
* @return 替換後的字符串
*/
public String replaceSpace(StringBuffer str) {
if (str == null || str.length() == 0) {
return str.toString();
}
int len = str.length();
for (int i = 0; i < len; ++i) {
if (str.charAt(i) == ' ') {
// append 兩個空格
str.append(" ");
}
}
// p 指向原字符串末尾
int p = len - 1;
// q 指向現字符串末尾
int q = str.length() - 1;
while (p >= 0) {
char ch = str.charAt(p--);
if (ch == ' ') {
str.setCharAt(q--, '0');
str.setCharAt(q--, '2');
str.setCharAt(q--, '%');
} else {
str.setCharAt(q--, ch);
}
}
return str.toString();
}
}
測試用例
- 輸入的字符串包含空格(空格位於字符串的最前面/最後面/中間;字符串有多個連續的空格);
- 輸入的字符串中沒有空格;
- 特殊輸入測試(字符串是一個空指針;字符串是一個空字符串;字符串只有一個空格字符;字符串中有多個連續空格)。
06_PrintListInReversedOrder
從尾到頭打印鏈表
題目描述
輸入一個鏈表,按鏈表值從尾到頭的順序返回一個 ArrayList
。
解法
解法一【推薦】
遍歷鏈表,每個鏈表結點值 push
進棧,最後將棧中元素依次 pop
到 list
中。
/**
* public class ListNode {
* int val;
* ListNode next = null;
*
* ListNode(int val) {
* this.val = val;
* }
* }
*
*/
import java.util.ArrayList;
import java.util.Stack;
/**
* @author bingo
* @since 2018/10/28
*/
public class Solution {
/**
* 從尾到頭打印鏈表
* @param listNode 鏈表頭節點
* @return list
*/
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList<>();
if (listNode == null) {
return res;
}
Stack<Integer> stack = new Stack<>();
while (listNode != null) {
stack.push(listNode.val);
listNode = listNode.next;
}
while (!stack.isEmpty()) {
res.add(stack.pop());
}
return res;
}
}
解法二【不推薦】
利用遞歸方式:
- 若不是鏈表尾結點,繼續遞歸;
- 若是,添加到
list
中。
這種方式不推薦,當遞歸層數過多時,容易發生 Stack Overflow
。
/**
* public class ListNode {
* int val;
* ListNode next = null;
*
* ListNode(int val) {
* this.val = val;
* }
* }
*
*/
import java.util.ArrayList;
import java.util.Stack;
/**
* @author bingo
* @since 2018/10/28
*/
public class Solution {
/**
* 從尾到頭打印鏈表
* @param listNode 鏈表頭結點
* @return list
*/
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList<>();
if (listNode == null) {
return res;
}
addElement(listNode, res);
return res;
}
private void addElement(ListNode listNode, ArrayList<Integer> res) {
if (listNode.next != null) {
// 遞歸調用
addElement(listNode.next, res);
}
res.add(listNode.val);
}
}
測試用例
- 功能測試(輸入的鏈表有多個結點;輸入的鏈表只有一個結點);
- 特殊輸入測試(輸入的鏈表結點指針爲空)。
07_ConstructBinaryTree
重建二叉樹
題目描述
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列 {1,2,4,7,3,5,6,8}
和中序遍歷序列 {4,7,2,1,5,3,8,6}
,則重建二叉樹並返回。
解法
在二叉樹的前序遍歷序列中,第一個數字總是根結點的值。在中序遍歷序列中,根結點的值在序列的中間,左子樹的結點位於根結點左側,而右子樹的結點位於根結點值的右側。
遍歷中序序列,找到根結點,遞歸構建左子樹與右子樹。
注意添加特殊情況的 if
判斷。
/**
* Definition for binary tree
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
/**
* @author bingo
* @since 2018/10/28
*/
public class Solution {
/**
* 重建二叉樹
*
* @param pre 先序序列
* @param in 中序序列
* @return 二叉樹根結點
*/
public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
if (pre == null || in == null || pre.length != in.length) {
return null;
}
int n = pre.length;
return constructBinaryTree(pre, 0, n - 1, in, 0, n - 1);
}
private TreeNode constructBinaryTree(int[] pre, int startPre, int endPre, int[] in, int startIn, int endIn) {
TreeNode node = new TreeNode(pre[startPre]);
if (startPre == endPre) {
if (startIn == endIn) {
return node;
}
throw new IllegalArgumentException("Invalid input!");
}
int inOrder = startIn;
while (in[inOrder] != pre[startPre]) {
++inOrder;
if (inOrder > endIn) {
new IllegalArgumentException("Invalid input!");
}
}
int len = inOrder - startIn;
if (len > 0) {
// 遞歸構建左子樹
node.left = constructBinaryTree(pre, startPre + 1, startPre + len, in, startIn, inOrder - 1);
}
if (inOrder < endIn) {
// 遞歸構建右子樹
node.right = constructBinaryTree(pre, startPre + len + 1, endPre, in, inOrder + 1, endIn);
}
return node;
}
}
測試用例
- 普通二叉樹(完全二叉樹;不完全二叉樹);
- 特殊二叉樹(所有結點都沒有左/右子結點;只有一個結點的二叉樹);
- 特殊輸入測試(二叉樹根結點爲空;輸入的前序序列和中序序列不匹配)。
08_NextNodeInBinaryTrees
二叉樹的下一個結點
題目描述
給定一個二叉樹和其中的一個結點,請找出中序遍歷順序的下一個結點並且返回。注意,樹中的結點不僅包含左右子結點,同時包含指向父結點的指針。
解法
對於結點 pNode
:
- 如果它有右子樹,則右子樹的最左結點就是它的下一個結點;
- 如果它沒有右子樹,判斷它與父結點
pNode.next
的位置情況:- 如果它是父結點的左孩子,那麼父結點
pNode.next
就是它的下一個結點; - 如果它是父結點的右孩子,一直向上尋找,直到找到某個結點,它是它父結點的左孩子,那麼該父結點就是
pNode
的下一個結點。
- 如果它是父結點的左孩子,那麼父結點
/*
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
*/
/**
* @author bingo
* @since 2018/10/28
*/
public class Solution {
/**
* 獲取中序遍歷結點的下一個結點
* @param pNode 某個結點
* @return pNode的下一個結點
*/
public TreeLinkNode GetNext(TreeLinkNode pNode) {
if (pNode == null) {
return null;
}
if (pNode.right != null) {
TreeLinkNode t = pNode.right;
while (t.left != null) {
t = t.left;
}
return t;
}
// 須保證 pNode.next 不爲空,否則會出現 NPE
if (pNode.next != null && pNode.next.left == pNode) {
return pNode.next;
}
while (pNode.next != null) {
if (pNode.next.left == pNode) {
return pNode.next;
}
pNode = pNode.next;
}
return null;
}
}
測試用例
- 普通二叉樹(完全二叉樹;不完全二叉樹);
- 特殊二叉樹(所有結點都沒有左/右子結點;只有一個結點的二叉樹;二叉樹的根結點爲空);
- 不同位置的結點的下一個結點(下一個結點爲當前結點的右子結點、右子樹的最左子結點、父結點、跨層的父結點等;當前結點沒有下一個結點)。
09_01_QueueWithTwoStacks
用兩個棧實現隊列
題目描述
用兩個棧來實現一個隊列,完成隊列的 Push
和 Pop
操作。 隊列中的元素爲 int
類型。
解法
Push
操作,每次都存入 stack1
;
Pop
操作,每次從 stack2
取:
stack2
棧不爲空時,不能將stack1
元素倒入;stack2
棧爲空時,需要一次將stack1
元素全部倒入。
import java.util.Stack;
/**
* @author bingo
* @since 2018/10/28
*/
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if (stack2.isEmpty()) {
if (stack1.isEmpty()) {
return -1;
}
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
測試用例
- 往空的隊列裏添加、刪除元素;
- 往非空的隊列添加、刪除元素;
- 連續刪除元素直至隊列爲空。
09_02_StackWithTwoQueues
用兩個隊列實現棧
題目描述
用兩個隊列來實現一個棧,完成棧的 Push
和 Pop
操作。 棧中的元素爲 int
類型。
解法
Push
操作,每次都存入 queue1
;
Pop
操作,每次從 queue1
取:
- 將
queue1
中的元素依次倒入queue2
,直到queue1
剩下一個元素,這個元素就是要pop
出去的; - 將
queue1
與queue2
進行交換,這樣保證每次都從queue1
中存取元素,queue2
只起到輔助暫存的作用。
import java.util.LinkedList;
import java.util.Queue;
/**
* @author bingo
* @since 2018/10/29
*/
public class Solution {
private Queue<Integer> queue1 = new LinkedList<>();
private Queue<Integer> queue2 = new LinkedList<>();
public void push(int node) {
queue1.offer(node);
}
public int pop() {
if (queue1.isEmpty()) {
throw new RuntimeException("Empty stack!");
}
while (queue1.size() > 1) {
queue2.offer(queue1.poll());
}
int val = queue1.poll();
Queue<Integer> t = queue1;
queue1 = queue2;
queue2 = t;
return val;
}
}
測試用例
- 往空的棧裏添加、刪除元素;
- 往非空的棧添加、刪除元素;
- 連續刪除元素直至棧爲空。
10_01_Fibonacci
斐波那契數列
題目描述
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第 n
項(從 0
開始,第 0
項爲 0
)。n<=39
解法
解法一
採用遞歸方式,簡潔明瞭,但效率很低,存在大量的重複計算。
f(10)
/ \
f(9) f(8)
/ \ / \
f(8) f(7) f(7) f(6)
/ \ / \
f(7) f(6) f(6) f(5)
/**
* @author bingo
* @since 2018/10/29
*/
public class Solution {
/**
* 求斐波那契數列的第n項,n從0開始
* @param n 第n項
* @return 第n項的值
*/
public int Fibonacci(int n) {
if (n < 2) {
return n;
}
// 遞歸調用
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
解法二
從下往上計算,遞推,時間複雜度 O(n)
。
/**
* @author bingo
* @since 2018/10/29
*/
public class Solution {
/**
* 求斐波那契數列的第n項,n從0開始
* @param n 第n項
* @return 第n項的值
*/
public int Fibonacci(int n) {
if (n < 2) {
return n;
}
int[] res = new int[n + 1];
res[0] = 0;
res[1] = 1;
for (int i = 2; i <= n; ++i) {
res[i] = res[i - 1] + res[i - 2];
}
return res[n];
}
}
測試用例
- 功能測試(如輸入 3、5、10 等);
- 邊界值測試(如輸入 0、1、2);
- 性能測試(輸入較大的數字,如 40、50、100 等)。
10_02_JumpFloor
跳臺階
題目描述
一隻青蛙一次可以跳上1
級臺階,也可以跳上2
級。求該青蛙跳上一個n
級的臺階總共有多少種跳法(先後次序不同算不同的結果)。
解法
跳上 n
級臺階,可以從 n-1
級跳 1
級上去,也可以從 n-2
級跳 2
級上去。所以
f(n) = f(n-1) + f(n-2)
/**
* @author bingo
* @since 2018/11/23
*/
public class Solution {
/**
* 青蛙跳臺階
* @param target 跳上的那一級臺階
* @return 多少種跳法
*/
public int JumpFloor(int target) {
if (target < 3) {
return target;
}
int[] res = new int[target + 1];
res[1] = 1;
res[2] = 2;
for (int i = 3; i <= target; ++i) {
res[i] = res[i - 1] + res[i - 2];
}
return res[target];
}
}
測試用例
- 功能測試(如輸入 3、5、10 等);
- 邊界值測試(如輸入 0、1、2);
- 性能測試(輸入較大的數字,如 40、50、100 等)。
10_03_JumpFloorII
變態跳臺階
題目描述
一隻青蛙一次可以跳上1
級臺階,也可以跳上2
級……它也可以跳上n
級。求該青蛙跳上一個n
級的臺階總共有多少種跳法。
解法
跳上 n-1
級臺階,可以從 n-2
級跳 1
級上去,也可以從 n-3
級跳 2
級上去…也可以從 0
級跳上去。那麼
f(n-1) = f(0) + f(1) + ... + f(n-2) ①
跳上 n
級臺階,可以從 n-1
級跳 1
級上去,也可以從 n-2
級跳 2
級上去…也可以從 0
級跳上去。那麼
f(n) = f(0) + f(1) + ... + f(n-2) + f(n-1) ②
②-①:
f(n) - f(n-1) = f(n-1)
f(n) = 2f(n-1)
所以 f(n) 是一個等比數列:
f(n) = 2^(n-1)
/**
* @author bingo
* @since 2018/11/23
*/
public class Solution {
/**
* 青蛙跳臺階II
* @param target 跳上的那一級臺階
* @return 多少種跳法
*/
public int JumpFloorII(int target) {
return (int) Math.pow(2, target - 1);
}
}
測試用例
- 功能測試(如輸入 3、5、10 等);
- 邊界值測試(如輸入 0、1、2);
- 性能測試(輸入較大的數字,如 40、50、100 等)。
10_04_RectCover
矩形覆蓋
題目描述
我們可以用2*1
的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n
個2*1
的小矩形無重疊地覆蓋一個2*n
的大矩形,總共有多少種方法?
解法
覆蓋 2*n
的矩形:
- 可以先覆蓋
2*n-1
的矩形,再覆蓋一個2*1
的矩形; - 也可以先覆蓋
2*(n-2)
的矩形,再覆蓋兩個1*2
的矩形。
解法一:利用數組存放結果
/**
* @author bingo
* @since 2018/11/23
*/
public class Solution {
/**
* 矩形覆蓋
* @param target 2*target大小的矩形
* @return 多少種覆蓋方法
*/
public int RectCover(int target) {
if (target < 3) {
return target;
}
int[] res = new int[target + 1];
res[1] = 1;
res[2] = 2;
for (int i = 3; i <= target; ++i) {
res[i] = res[i - 1] + res[i - 2];
}
return res[target];
}
}
解法二:直接用變量存儲結果
/**
* @author bingo
* @since 2018/11/23
*/
public class Solution {
/**
* 矩形覆蓋
* @param target 2*target大小的矩形
* @return 多少種覆蓋方法
*/
public int RectCover(int target) {
if (target < 3) {
return target;
}
int res1 = 1;
int res2 = 2;
int res = 0;
for (int i = 3; i <= target; ++i) {
res = res1 + res2;
res1 = res2;
res2 = res;
}
return res;
}
}
測試用例
- 功能測試(如輸入 3、5、10 等);
- 邊界值測試(如輸入 0、1、2);
- 性能測試(輸入較大的數字,如 40、50、100 等)。
11_MinNumberInRotatedArray
旋轉數組的最小數字
題目描述
把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組 {3,4,5,1,2}
爲 {1,2,3,4,5}
的一個旋轉,該數組的最小值爲 1
。
**NOTE:**給出的所有元素都大於 0
,若數組大小爲 0
,請返回 0
。
解法
解法一
直接遍歷數組找最小值,時間複雜度 O(n)
,不推薦。
/**
* @author bingo
* @since 2018/10/30
*/
public class Solution {
/**
* 獲取旋轉數組的最小元素
* @param array 旋轉數組
* @return 數組中的最小值
*/
public int minNumberInRotateArray(int[] array) {
if (array == null || array.length == 0) {
return 0;
}
int n = array.length;
if (n == 1 || array[0] < array[n - 1]) {
return array[0];
}
int min = array[0];
for (int i = 1; i < n; ++i) {
min = array[i] < min ? array[i] : min;
}
return min;
}
}
解法二
利用指針 p
,q
指向數組的首尾,如果 array[p] < array[q]
,說明數組是遞增數組,直接返回 array[p]
。否則進行如下討論。
計算中間指針 mid
:
- 如果此時
array[p]
,array[q]
,array[mid]
兩兩相等,此時無法採用二分方式,只能通過遍歷區間[p,q]
獲取最小值; - 如果此時
p
,q
相鄰,說明此時q
指向的元素是最小值,返回array[q]
; - 如果此時
array[mid] >= array[p]
,說明mid
位於左邊的遞增數組中,最小值在右邊,因此,把p
指向mid
,此時保持了p
指向左邊遞增子數組; - 如果此時
array[mid] <= array[q]
,說明mid
位於右邊的遞增數組中,最小值在左邊,因此,把q
指向mid
,此時保持了q
指向右邊遞增子數組。
/**
* @author bingo
* @since 2018/10/30
*/
public class Solution {
/**
* 獲取旋轉數組的最小元素
* @param array 旋轉數組
* @return 數組中的最小值
*/
public int minNumberInRotateArray(int[] array) {
if (array == null || array.length == 0) {
return 0;
}
int p = 0;
// mid初始爲p,爲了兼容當數組是遞增數組(即不滿足 array[p] >= array[q])時,返回 array[p]
int mid = p;
int q = array.length - 1;
while (array[p] >= array[q]) {
if (q - p == 1) {
// 當p,q相鄰時(距離爲1),那麼q指向的元素就是最小值
mid = q;
break;
}
mid = p + ((q - p) >> 1);
// 當p,q,mid指向的值相等時,此時只能通過遍歷查找最小值
if (array[p] == array[q] && array[mid] == array[p]) {
mid = getMinIndex(array, p, q);
break;
}
if (array[mid] >= array[p]) {
p = mid;
} else if (array[mid] <= array[q]) {
q = mid;
}
}
return array[mid];
}
private int getMinIndex(int[] array, int p, int q) {
int minIndex = p;
for (int i = p + 1; i <= q; ++i) {
minIndex = array[i] < array[minIndex] ? i : minIndex;
}
return minIndex;
}
}
測試用例
- 功能測試(輸入的數組是升序排序數組的一個旋轉,數組中有重複數字或者沒有重複數字);
- 邊界值測試(輸入的數組是一個升序排序的數組,只包含一個數字的數組);
- 特殊輸入測試(輸入空指針)。
12_StringPathInMatrix
矩陣中的路徑
題目描述
請設計一個函數,用來判斷在一個矩陣中是否存在一條包含某字符串所有字符的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則之後不能再次進入這個格子。 例如 a b c e s f c s a d e e
這樣的 3 X 4
矩陣中包含一條字符串"bcced"
的路徑,但是矩陣中不包含"abcb"
路徑,因爲字符串的第一個字符b佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。
解法
回溯法。首先,任選一個格子作爲路徑起點。假設格子對應的字符爲 ch,並且對應路徑上的第 i 個字符。若相等,到相鄰格子尋找路徑上的第 i+1 個字符。重複這一過程。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 判斷矩陣中是否包含某條路徑
* @param matrix 矩陣
* @param rows 行數
* @param cols 列數
* @param str 路徑
* @return bool
*/
public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
if (matrix == null || rows < 1 || cols < 1 || str == null) {
return false;
}
boolean[] visited = new boolean[matrix.length];
int pathLength = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (hasPath(matrix, rows, cols, str, i, j, pathLength, visited)) {
return true;
}
}
}
return false;
}
private boolean hasPath(char[] matrix, int rows, int cols, char[] str, int i, int j, int pathLength, boolean[] visited) {
if (pathLength == str.length) {
return true;
}
boolean hasPath = false;
if (i >= 0 && i < rows && j >= 0 && j < cols && matrix[i * cols + j] == str[pathLength] && !visited[i * cols + j]) {
++pathLength;
visited[i * cols + j] = true;
hasPath = hasPath(matrix, rows, cols, str, i - 1, j, pathLength, visited)
|| hasPath(matrix, rows, cols, str, i + 1, j, pathLength, visited)
|| hasPath(matrix, rows, cols, str, i, j - 1, pathLength, visited)
|| hasPath(matrix, rows, cols, str, i, j + 1, pathLength, visited);
if (!hasPath) {
--pathLength;
visited[i * cols + j] = false;
}
}
return hasPath;
}
}
測試用例
- 功能測試(在多行多列的矩陣中存在或者不存在路徑);
- 邊界值測試(矩陣只有一行或者一列;矩陣和路徑中的所有字母都是相同的);
- 特殊輸入測試(輸入空指針)。
13_RobotMove
機器人的移動範圍
題目描述
地上有一個m
行和n
列的方格。一個機器人從座標0,0
的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是不能進入行座標和列座標的數位之和大於k
的格子。 例如,當k
爲18
時,機器人能夠進入方格(35,37)
,因爲3+5+3+7 = 18
。但是,它不能進入方格(35,38)
,因爲3+5+3+8 = 19
。請問該機器人能夠達到多少個格子?
解法
從座標(0, 0) 開始移動,當它準備進入座標(i, j),判斷是否能進入,如果能,再判斷它能否進入 4 個相鄰的格子 (i-1, j), (i+1, j), (i, j-1), (i, j+1)。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 計算能到達的格子數
* @param threshold 限定的數字
* @param rows 行數
* @param cols 列數
* @return 格子數
*/
public int movingCount(int threshold, int rows, int cols) {
if (threshold < 0 || rows < 1 || cols < 1) {
return 0;
}
boolean[] visited = new boolean[rows * cols];
return getCount(threshold, 0, 0, rows, cols, visited);
}
private int getCount(int threshold, int i, int j, int rows, int cols, boolean[] visited) {
if (check(threshold, i, j, rows, cols, visited)) {
visited[i * cols + j] = true;
return 1
+ getCount(threshold, i - 1, j, rows, cols, visited)
+ getCount(threshold, i + 1, j, rows, cols, visited)
+ getCount(threshold, i, j - 1, rows, cols, visited)
+ getCount(threshold, i, j + 1, rows, cols, visited);
}
return 0;
}
private boolean check(int threshold, int i, int j, int rows, int cols, boolean[] visited) {
return i >= 0
&& i < rows
&& j >= 0
&& j < cols
&& !visited[i * cols + j]
&& getDigitSum(i) + getDigitSum(j) <= threshold;
}
private int getDigitSum(int i) {
int res = 0;
while (i > 0) {
res += i % 10;
i /= 10;
}
return res;
}
}
測試用例
- 功能測試(方格爲多行多列;k 爲正數);
- 邊界值測試(方格只有一行或者一列;k = 0);
- 特殊輸入測試(k < 0)。
14_CuttingRope
剪繩子
題目描述
給你一根長度爲n
繩子,請把繩子剪成m
段(m
、n
都是整數,n>1
並且m≥1
)。每段的繩子的長度記爲k[0]、k[1]、……、k[m]。k[0]k[1]…*k[m]可能的最大乘積是多少?例如當繩子的長度是 8 時,我們把它剪成長度分別爲 2、3、3
的三段,此時得到最大的乘積18
。
解法
解法一:動態規劃法
時間複雜度O(n²)
,空間複雜度O(n)
。
- 長度爲 2,只可能剪成長度爲 1 的兩段,因此 f(2)=1
- 長度爲 3,剪成長度分別爲 1 和 2 的兩段,乘積比較大,因此 f(3) = 2
- 長度爲 n,在剪第一刀的時候,有 n-1 種可能的選擇,剪出來的繩子又可以繼續剪,可以看出,原問題可以劃分爲子問題,子問題又有重複子問題。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 剪繩子求最大乘積
* @param length 繩子長度
* @return 乘積最大值
*/
public int maxProductAfterCutting(int length) {
if (length < 2) {
return 0;
}
if (length < 4) {
return length - 1;
}
// res[i] 表示當長度爲i時的最大乘積
int[] res = new int[length + 1];
res[1] = 1;
res[2] = 2;
res[3] = 3;
// 從長度爲4開始計算
for (int i = 4; i <= length; ++i) {
int max = 0;
for (int j = 1; j <= i / 2; ++j) {
max = Math.max(max, res[j] * res[i - j]);
}
res[i] = max;
}
return res[length];
}
}
貪心算法
時間複雜度O(1)
,空間複雜度O(1)
。
貪心策略:
- 當 n>=5 時,儘可能多地剪長度爲 3 的繩子
- 當剩下的繩子長度爲 4 時,就把繩子剪成兩段長度爲 2 的繩子。
證明:
- 當 n>=5 時,可以證明 2(n-2)>n,並且 3(n-3)>n。也就是說,當繩子剩下長度大於或者等於 5 的時候,可以把它剪成長度爲 3 或者 2 的繩子段。
- 當 n>=5 時,3(n-3)>=2(n-2),因此,應該儘可能多地剪長度爲 3 的繩子段。
- 當 n=4 時,剪成兩根長度爲 2 的繩子,其實沒必要剪,只是題目的要求是至少要剪一刀。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 剪繩子求最大乘積
* @param length 繩子長度
* @return 乘積最大值
*/
public int maxProductAfterCutting(int length) {
if (length < 2) {
return 0;
}
if (length < 4) {
return length - 1;
}
int timesOf3 = length / 3;
if (length % 3 == 1) {
--timesOf3;
}
int timesOf2 = (length - timesOf3 * 3) >> 1;
return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
}
}
測試用例
- 功能測試(繩子的初始長度大於 5);
- 邊界值測試(繩子的初始長度分別爲 0、1、2、3、4)。
15_NumberOf1InBinary
二進制中 1 的個數
題目描述
輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。
解法
解法一
利用整數 1,依次左移每次與 n 進行與運算,若結果不爲0,說明這一位上數字爲 1,++cnt。
此解法 i 需要左移 32 次。
不要用 n 去右移並與 1 進行與運算,因爲n 可能爲負數,右移時會陷入死循環。
public class Solution {
public int NumberOf1(int n) {
int cnt = 0;
int i = 1;
while (i != 0) {
if ((n & i) != 0) {
++cnt;
}
i <<= 1;
}
return cnt;
}
}
解法二(推薦)
- 運算 (n - 1) & n,直至 n 爲 0。運算的次數即爲 n 的二進制中 1 的個數。
因爲 n-1 會將 n 的最右邊一位 1 改爲 0,如果右邊還有 0,則所有 0 都會變成 1。結果與 n 進行與運算,會去除掉最右邊的一個1。
舉個栗子:
若 n = 1100,
n - 1 = 1011
n & (n - 1) = 1000
即:把最右邊的 1 變成了 0。
把一個整數減去 1 之後再和原來的整數做位與運算,得到的結果相當於把整數的二進制表示中最右邊的 1 變成 0。很多二進制的問題都可以用這種思路解決。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 計算整數的二進制表示裏1的個數
* @param n 整數
* @return 1的個數
*/
public int NumberOf1(int n) {
int cnt = 0;
while (n != 0) {
n = (n - 1 ) & n;
++cnt;
}
return cnt;
}
}
測試用例
- 正數(包括邊界值 1、0x7FFFFFFF);
- 負數(包括邊界值 0x80000000、0xFFFFFFFF);
- 0。
16_Power
數值的整數次方
題目描述
給定一個 double
類型的浮點數 base
和 int
類型的整數 exponent
。求 base
的 exponent
次方。
解法
注意判斷值數是否小於 0。另外 0 的 0 次方沒有意義,也需要考慮一下,看具體題目要求。
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 計算數值的整數次方
* @param base 底數
* @param exponent 指數
* @return 數值的整數次方
*/
public double Power(double base, int exponent) {
double result = 1.0;
int n = Math.abs(exponent);
for (int i = 0; i < n; ++i) {
result *= base;
}
return exponent < 0 ? 1.0 / result : result;
}
}
測試用例
- 把底數和指數分別設爲正數、負數和零。
17_Print1ToMaxOfNDigits
打印從 1 到最大的 n 位數
題目描述
輸入數字 n
,按順序打印出從 1
最大的 n
位十進制數。比如輸入 3
,則打印出 1、2、3
一直到最大的 3 位數即 999。
解法
此題需要注意 n 位數構成的數字可能超出最大的 int 或者 long long 能表示的範圍。因此,採用字符數組來存儲數字。
關鍵是:
- 對字符數組表示的數進行遞增操作
- 輸出數字(0開頭的需要把0去除)
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
/**
* 打印從1到最大的n位數
* @param n n位
*/
public void print1ToMaxOfNDigits(int n) {
if (n < 1) {
return;
}
char[] chars = new char[n];
for (int i = 0; i < n; ++i) {
chars[i] = '0';
}
while (!increment(chars)) {
printNumber(chars);
}
}
/**
* 打印數字(去除前面的0)
* @param chars 數字數組
*/
private void printNumber(char[] chars) {
int index = 0;
int n = chars.length;
for (char ch : chars) {
if (ch != '0') {
break;
}
++index;
}
StringBuilder sb = new StringBuilder();
for (int i = index; i < n; ++i) {
sb.append(chars[i]);
}
System.out.println(sb.toString());
}
/**
* 數字加1
* @param chars 數字數組
* @return 是否溢出
*/
private boolean increment(char[] chars) {
boolean flag = false;
int n = chars.length;
int carry = 1;
for (int i = n - 1; i >= 0; --i) {
int num = chars[i] - '0' + carry;
if (num > 9) {
if (i == 0) {
flag = true;
break;
}
chars[i] = '0';
} else {
++chars[i];
break;
}
}
return flag;
}
}
測試用例
- 功能測試(輸入 1、2、3…);
- 特殊輸入測試(輸入 -1、0)。
18_01_DeleteNodeInList
在O(1)時間內刪除鏈表節點
題目描述
給定單向鏈表的頭指針和一個節點指針,定義一個函數在 O(1) 時間內刪除該節點。
解法
判斷要刪除的節點是否是尾節點,若是,直接刪除;若不是,把要刪除節點的下一個節點賦給要刪除的節點即可。
進行n次操作,平均時間複雜度爲:( (n-1) * O(1) + O(n) ) / n = O(1),所以符合題目上說的O(1)
/**
* @author bingo
* @since 2018/11/20
*/
public class Solution {
class ListNode {
int val;
ListNode next;
}
/**
* 刪除鏈表的節點
* @param head 鏈表頭節點
* @param tobeDelete 要刪除的節點
*/
public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
if (head == null || tobeDelete == null) {
return head;
}
// 刪除的不是尾節點
if (tobeDelete.next != null) {
tobeDelete.val = tobeDelete.next.val;
tobeDelete.next = tobeDelete.next.next;
}
// 鏈表中僅有一個節點
else if (head == tobeDelete) {
head = null;
}
// 刪除的是尾節點
else {
ListNode ptr = head;
while (ptr.next != tobeDelete) {
ptr = ptr.next;
}
ptr.next = null;
}
return head;
}
}
測試用例
- 功能測試(從有多個節點的鏈表的中間/頭部/尾部刪除一個節點;從只有一個節點的鏈表中刪除唯一的節點);
- 特殊輸入測試(指向鏈表頭節點的爲空指針;指向要刪除節點的爲空指針)。
18_02_DeleteDuplicatedNode
刪除鏈表中重複的節點
題目描述
在一個排序的鏈表中,存在重複的結點,請刪除該鏈表中重複的結點,重複的結點不保留,返回鏈表頭指針。 例如,鏈表1->2->3->3->4->4->5
處理後爲 1->2->5
。
解法
解法一:遞歸
/**
* @author bingo
* @since 2018/11/21
*/
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
/**
* 刪除鏈表重複的節點
* @param pHead 鏈表頭節點
* @return 刪除節點後的鏈表
*/
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null) {
return pHead;
}
if (pHead.val == pHead.next.val) {
if (pHead.next.next == null) {
return null;
}
if (pHead.next.next.val == pHead.val) {
return deleteDuplication(pHead.next);
}
return deleteDuplication(pHead.next.next);
}
pHead.next = deleteDuplication(pHead.next);
return pHead;
}
}
解法二
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null) {
return pHead;
}
ListNode pre = null;
ListNode cur = pHead;
while (cur != null) {
if (cur.next != null && cur.next.val == cur.val) {
int val = cur.val;
while (cur.next != null && cur.next.val == val) {
cur = cur.next;
}
if (pre == null) {
pHead = cur.next;
} else {
pre.next = cur.next;
}
} else {
pre = cur;
}
cur = cur.next;
}
return pHead;
}
}
測試用例
- 功能測試(重複的節點位於鏈表的頭部/中間/尾部;鏈表中沒有重複的節點);
- 特殊輸入測試(指向鏈表頭節點的爲空指針;鏈表中所有節點都是重複的)。
19_RegularExpressionsMatching
正則表達式匹配
題目描述
請實現一個函數用來匹配包括.
和*
的正則表達式。模式中的字符.
表示任意一個字符,而*
表示它前面的字符可以出現任意次(包含0
次)。 在本題中,匹配是指字符串的所有字符匹配整個模式。例如,字符串aaa
與模式a.a
和ab*ac*a
匹配,但是與aa.a
和ab*a
均不匹配。
解法
判斷模式中第二個字符是否是 *
:
- 若是,看如果模式串第一個字符與字符串第一個字符是否匹配:
-
- 若不匹配,在模式串上向右移動兩個字符
j+2
,相當於 a* 被忽略
- 若不匹配,在模式串上向右移動兩個字符
-
- 若匹配,字符串後移
i+1
。此時模式串可以移動兩個字符j+2
,也可以不移動j
。
- 若匹配,字符串後移
-
- 若不是,看當前字符與模式串的當前字符是否匹配,即 str[i] == pattern[j] || pattern[j] == ‘.’:
-
- 若匹配,則字符串與模式串都向右移動一位,
i+1
,j+1
。
- 若匹配,則字符串與模式串都向右移動一位,
-
- 若不匹配,返回 fasle。
-
/**
* @author bingo
* @since 2018/11/21
*/
public class Solution {
/**
* 判斷字符串是否與模式串匹配
* @param str 字符串
* @param pattern 模式串
* @return 是否匹配
*/
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) {
return false;
}
return match(str, 0, str.length, pattern, 0, pattern.length);
}
private boolean match(char[] str, int i, int len1,
char[] pattern, int j, int len2) {
if (i == len1 && j == len2) {
return true;
}
// "",".*"
if (i != len1 && j == len2) {
return false;
}
if (j + 1 < len2 && pattern[j + 1] == '*') {
if (i < len1 && (str[i] == pattern[j] || pattern[j] == '.')) {
return match(str, i, len1, pattern, j + 2, len2)
|| match(str, i + 1, len1, pattern, j, len2)
|| match(str, i + 1, len1, pattern,j + 2, len2);
}
// "",".*"
return match(str, i, len1, pattern, j + 2, len2);
}
if (i < len1 && (str[i] == pattern[j] || pattern[j] == '.')) {
return match(str, i + 1, len1, pattern, j + 1, len2);
}
return false;
}
}
測試用例
- 功能測試(模式字符串裏包含普通字符、
.
、*
;模式字符串和輸入字符串匹配/不匹配); - 特殊輸入測試(輸入字符串和模式字符串是空指針、空字符串)。
20_NumericStrings
表示數值的字符串
題目描述
請實現一個函數用來判斷字符串是否表示數值(包括整數和小數)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示數值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
解法
解法一
利用正則表達式匹配即可。
[] : 字符集合
() : 分組
? : 重複 0 ~ 1
+ : 重複 1 ~ n
* : 重複 0 ~ n
. : 任意字符
\\. : 轉義後的 .
\\d : 數字
/**
* @author bingo
* @since 2018/11/21
*/
public class Solution {
/**
* 判斷是否是數字
* @param str
* @return
*/
public boolean isNumeric(char[] str) {
return str != null
&& str.length != 0
&& new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}
}
解法二【劍指offer解法】
表示數值的字符串遵循模式A[.[B]][e|EC]
或者.B[e|EC]
,其中A爲數值的整數部分,B緊跟小數點爲數值的小數部分,C緊跟着e或者E爲數值的指數部分。上述A和C都有可能以 +
或者 -
開頭的09的數位串,B也是09的數位串,但前面不能有正負號。
/**
* @author mcrwayfun
* @version v1.0
* @date Created in 2018/12/29
* @description
*/
public class Solution {
private int index = 0;
/**
* 判斷是否是數值
* @param str
* @return
*/
public boolean isNumeric(char[] str) {
if (str == null || str.length < 1) {
return false;
}
// 判斷是否存在整數
boolean flag = scanInteger(str);
// 小數部分
if (index < str.length && str[index] == '.') {
index++;
// 小數部分可以有整數或者沒有整數
// 所以使用 ||
flag = scanUnsignedInteger(str) || flag;
}
if (index < str.length && (str[index] == 'e' || str[index] == 'E')) {
index++;
// e或E前面必須有數字
// e或者E後面必須有整數
// 所以使用 &&
flag = scanInteger(str) && flag;
}
return flag && index == str.length;
}
private boolean scanInteger(char[] str) {
// 去除符號
while (index < str.length && (str[index] == '+' || str[index] == '-')) {
index++;
}
return scanUnsignedInteger(str);
}
private boolean scanUnsignedInteger(char[] str) {
int start = index;
while (index < str.length && str[index] >= '0' && str[index] <= '9') {
index++;
}
// 判斷是否存在整數
return index > start;
}
}
測試用例
- 功能測試(正數或者負數;包含或者不包含整數部分的數值;包含或者不包含效數部分的值;包含或者不包含指數部分的值;各種不能表達有效數值的字符串);
- 特殊輸入測試(輸入字符串和模式字符串是空指針、空字符串)。