劍指offer——java刷題總結【四】

Note

  • 題解彙總:劍指offer題解彙總
  • 代碼地址:Github 劍指offer Java實現彙總
  • 點擊目錄中的題名鏈接可直接食用題解~
  • 有些解法博文中未實現,不代表一定很難,可能只是因爲博主太懶```(Orz)
  • 如果博文中有明顯錯誤或者某些題目有更加優雅的解法請指出,謝謝~

目錄

題號 題目名稱
31 從1到n整數中1出現的次數
32 把數組排成最小的數
33 醜數
34 第一個只出現一次的字符
35 數組中的逆序對
36 兩個鏈表的第一個公共節點
37 數字在排序數組中出現的次數
38 二叉樹的深度
39 平衡二叉樹
40 數組中只出現一次的數字

正文

31、從1到n整數中1出現的次數

題目描述

求出1~13的整數中1出現的次數,並算出100~1300的整數中1出現的次數?爲此他特別數了一下1~13中包含1的數字有1、10、11、12、13因此共出現6次,但是對於後面問題他就沒轍了。ACMer希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整數區間中1出現的次數(從1 到 n 中1出現的次數)。

題目分析

解法一:
以n=216爲例:
個位上:1 ,11,21,31,…211。個位上共出現(216/10)+ 1個 1 。因爲除法取整,210~216間個位上的1取不到,所以我們加8進位。你可能說爲什麼不加9,n=211怎麼辦,這裏把最後取到的個位數爲1的單獨考慮,先往下看。
十位上:10~19,110~119,210~216。十位上可看成求(216/10)=21個位上的1的個數然後乘10。這裏再次把最後取到的十位數爲1的單獨拿出來,即210~216要單獨考慮 ,個數爲(216%10)+1 .這裏加8就避免了判斷的過程。
後面以此類推。

m 1 10 100
a 216 21 2
b 0 6 16
count 22+0 20+7 100+0
代碼實現

解法一: O(lgn)

public int NumberOf1Between1AndN_Solution(int n) {
    int count = 0;
    for (int m = 1; m <= n; m *= 10) {
        int a = n / m;
        int b = n % m;
        count += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
    }
    return count;
}

32、把數組排成最小的數

題目描述

輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。例如輸入數組{3,32,321},則打印出這三個數字能排成的最小數字爲321323。

題目分析

解法一: 貪心策略。自定義比較器,對a+b和b+a形成的數字進行比較。時間複雜度主要在排序。

代碼實現

解法一: O(n²)

public String PrintMinNumber(int [] numbers) {
    String[] num = new String[numbers.length];
    for (int i = 0; i < numbers.length; i++) {
        num[i] = String.valueOf(numbers[i]);
    }
    Arrays.sort(num, new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return (o1 + o2).compareTo(o2 + o1);
        }
    });
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < num.length; i++) {
        sb.append(num[i]);
    }
    return sb.toString();
}

33、醜數

題目描述

把只包含質因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因爲它包含質因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。

題目分析

解法一: 動態規劃。dp[i] 表示第i個醜數,其狀態轉移方程如下:
dp[i] = min(2 * dp[dp2], 3 * dp[dp3], 5 * dp[dp5])
dp2、dp3和dp5都是指針,初始值指向dp[0]。由於醜數都是由2、3、5組成的,所以醜數都是由其它醜數乘以2、3、5組成的,我們初始化三個指針指向第一個醜數1,分別對它進行乘以2、3、5的操作,獲取最小的那個數字作爲新的醜數,並將對應的乘數+1,因爲當前醜數作爲乘數時已經被使用過了,應該使用更大的一個醜數作爲下一次做乘法計算的乘數。時間複雜度:O(n) 空間複雜度:O(n)

求某個特殊的數時比如happy數、sad數都可以考慮使用這種解法,對該類數字從小到大進行擴充。

代碼實現

解法一: O(n)

public int GetUglyNumber_Solution(int index) {
    if (index == 0) return 0;
    int[] num = new int[index + 1];
    num[1] = 1;
    int index2 = 1;
    int index3 = 1;
    int index5 = 1;
    for (int i = 2; i <= index; i++) {
        num[i] = Math.min(num[index2] * 2, num[index3] * 3);
        num[i] = Math.min(num[i], num[index5] * 5);
        if (num[i] == num[index5] * 5) {
            index5++;
        }
        if (num[i] == num[index3] * 3) {
            index3++;
        }
        if (num[i] == num[index2] * 2) {
            index2++;
        }
    }
    return num[index];
}

34、第一個只出現一次的字符

題目描述

在一個字符串(0<=字符串長度<=10000,全部由字母組成)中找到第一個只出現一次的字符,並返回它的位置, 如果沒有則返回 -1(需要區分大小寫)。

題目分析

解法一: 建立一個哈希表,第一次掃描的時候,統計每個字符的出現次數。第二次掃描的時候,如果該字符出現的次數爲1,則返回這個字符的位置。時間複雜度O(n)

代碼實現

解法一: O(n)

public int FirstNotRepeatingChar(String str) {
    if (str == null || str.length() == 0) return -1;
    Map<Character, Integer> map = new LinkedHashMap<>();
    char[] chars = str.toCharArray();
    for (int i = 0; i < chars.length; i++) {
        map.put(chars[i], map.getOrDefault(chars[i], 0) + 1);
    }
    for (int i = 0; i < chars.length; i++) {
        if (map.get(chars[i]) == 1) {
            return i;
        }
    }
    return -1;
}

35、數組中的逆序對

題目描述

在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數P。並將P對1000000007取模的結果輸出。即輸出P%1000000007。

輸入描述

題目保證輸入的數組中沒有的相同的數字。
數據範圍:
對於%50的數據,size<=10^4
對於%75的數據,size<=10^5
對於%100的數據,size<=2*10^5

題目分析

解法一: 使用歸併排序的思想,在merge階段做一下逆序對的判斷,對count進行累加即可。

代碼實現

解法一: O(n²)

public static int count = 0;
public static int InversePairs(int [] array) {
    if (array == null || array.length == 0) return 0;
    countSum(array, 0, array.length - 1);
    return count;
}

public static void countSum(int[] array, int L, int R) {
    if (L == R) {
        return;
    }
    int mid = L + ((R - L) >> 1);
    countSum(array, L, mid);
    countSum(array, mid + 1, R);
    merge(array, L, mid, R);
}

public static void merge(int[] array, int left, int mid, int right) {
    int[] help = new int[right - left + 1];
    int p1 = left;
    int p2 = mid + 1;
    int i = 0;
    while (p1 <= mid && p2 <= right) {
        count = array[p1] <= array[p2] ? (count % 1000000007) : (count + (mid - p1 + 1) % 1000000007);
        help[i++] = array[p1] <= array[p2] ? array[p1++] : array[p2++];
    }
    while (p1 <= mid) {
        help[i++] = array[p1++];
    }
    while (p2 <= right) {
        help[i++] = array[p2++];
    }
    for (i = 0; i < help.length; i++) {
        array[left + i] = help[i];
    }
}

36、兩個鏈表的第一個公共節點

題目描述

輸入兩個鏈表,找出它們的第一個公共結點。

題目分析

解法一: 雙指針法。創建兩個指針p1和p2,分別指向兩個鏈表的頭結點,然後依次往後遍歷。如果某個指針到達末尾,則將該指針指向另一個鏈表的頭結點;如果兩個指針所指的節點相同,則循環結束,返回當前指針指向的節點。比如兩個鏈表分別爲:1->3->4->5->6和2->7->8->9->5->6。短鏈表的指針p1會先到達尾部,然後重新指向長鏈表頭部,當長鏈表的指針p2到達尾部時,重新指向短鏈表頭部,此時p1在長鏈表中已經多走了k步(k爲兩個鏈表的長度差值),p1和p2位於同一起跑線,往後遍歷找到相同節點即可。其實該方法主要就是用鏈表循環的方式替代了長鏈表指針先走k步這一步驟。

代碼實現

解法一: O(m+n)

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    if (pHead1 == null || pHead2 == null) return null;
    ListNode p1 = pHead1;
    ListNode p2 = pHead2;
    while (p1 != p2) {
        p1 = p1 == null ? pHead2 : p1.next;
        p2 = p2 == null ? pHead1 : p2.next;
    }
    return p1;
}

37、數字在排序數組中出現的次數

題目描述

統計一個數字在排序數組中出現的次數。

題目分析

解法一: 數組有序用二分法查找該數字的某個出現位置,把時間複雜度壓到logn,然後對查找到的index前後遍歷累加相等值的出現次數即可。最終的時間複雜度和該數字的選擇有關,大部分情況下都是在O(logn)內實現的,最壞情況爲O(n)。不願意自己手擼二分查找的,使用Arrays.binarySearch(arr, k)獲得也可。

代碼實現

解法一: O(n)

public int GetNumberOfK(int [] array , int k) {
    if (array.length == 0 || array == null) return 0;
    int left = 0;
    int right = array.length;
    int mid;
    int index = -1;
    while (left < right) {
        mid = left + ((right - left) >> 1);
        if (array[mid] < k) {
            left = mid + 1;
        } else if (array[mid] > k) {
            right = mid;
        } else {
            index = mid;
            break;
        }
    }
    if (index == -1) return 0;
    int count = 0;
    int t1 = index;
    int t2 = index + 1;
    while (t1 >= 0 && array[t1] == k) {
        count++;
        t1--;
    }
    while (t2 < array.length && array[t2] == k) {
        count++;
        t2++;
    }
    return count;
}

38、二叉樹的深度

題目描述

輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度爲樹的深度。

題目分析

解法一: 遞歸。其實子問題就是左子樹高度和右子樹高度之中的較大值加上當前節點,也就是高度+1。如果遞歸到的節點爲空則返回0。每個節點只會訪問一次,所以時間複雜度爲O(n),空間開銷爲整棵遞歸樹的高度logn。時間複雜度:O(n) 空間複雜度:O(logn)
解法二: 非遞歸。使用層序遍歷的思想,利用隊列,把每一層的節點都彈出後,在加入下一層的節點時count++,可以使用隊列的size來判斷當前層的節點是否都被彈出。空間開銷是樹中包含節點數最多的那一層的節點個數(大部分情況下應該是最後一層或者倒數第二層)。時間複雜度:O(n)

代碼實現

解法一: O(n)

public int TreeDepth(TreeNode root) {
    if (root == null) return 0;
    return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
}

解法二: O(n)

public static int TreeDepth1(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    int count = 0;
    int levelCount = 0;
    int qSize = queue.size();
    while (!queue.isEmpty()) {
        TreeNode t = queue.poll();
        levelCount++;
        if (t.left != null) queue.add(t.left);
        if (t.right != null) queue.add(t.right);
        if (levelCount == qSize) {
            levelCount = 0;
            qSize = queue.size();
            count++;
        }
    }
    return count;
}

39、平衡二叉樹

題目描述

輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。

題目分析

解法一: 遞歸。如果當前節點爲null,也說明是平衡樹,返回true。如果左子樹和右子樹的高度差大於1,說明以當前節點作爲根節點的樹不是平衡樹,則直接返回false;如果左子樹和右子樹的高度差小於等於1,說明以當前節點作爲根節點的樹是平衡樹,則遞歸判斷對其左子樹和右子樹是否是平衡樹。計算樹的高度可參考上一題的解答。時間複雜度暫時還有點凌亂…(Master Method可計算遞歸的複雜度,需惡補)

代碼實現

解法一:

public boolean IsBalanced_Solution(TreeNode root) {
    if (root == null) return true;
    if (Math.abs(TreeDepth(root.left) - TreeDepth(root.right)) > 1) {
        return false;
    } else {
        return IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
    }
}

public int TreeDepth(TreeNode root) {
    if (root == null) return 0;
    return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
}

40、數組中只出現一次的數字

題目描述

一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。

題目分析

解法一: 哈希表。遍歷數組,如果哈希表中存在該元素,說明該元素出現次數爲2,則將該元素移除;如果哈希表中不存在該元素,則將該元素添加到哈希表。最後哈希表中留下的數字就是隻出現一次數數字。
解法二: 位運算。遍歷數組,將所有元素進行異或操作,由於異或相等爲0,不等爲1,因此最終的異或結果是隻出現一次的數字a和b異或的結果r=a^b。然後我們可以基於r的二進制,根據第一位爲1的位數,將原數組分爲兩組,分組的標準就是該位是否爲1。相同的數字二進制數也一樣,所以一定在一個組,而不同的數字肯定在不同的組,這樣就能把a和b劃分到兩個不同組中。再對這兩個組按照最開始的思路,分別求異或,最終的結果就是這兩個只出現一次的數字。時間複雜度雖然一樣,但是異或操作的指令完成速度更快。

代碼實現

解法一: O(n)

public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
    if (array == null || array.length == 0) return;
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < array.length; i++) {
        if (!set.contains(array[i])) {
            set.add(array[i]);
        } else {
            set.remove(array[i]);
        }
    }
    Iterator<Integer> it = set.iterator();
    num1[0] = it.next();
    num2[0] = it.next();
}

解法二: O(n)

public void FindNumsAppearOnce1(int [] array,int num1[] , int num2[]) {
    if (array == null || array.length == 0) return;
    int xor = 0;
    for (int i = 0; i < array.length; i++) {
        xor ^= array[i];
    }
    int index = 1;
    while ((xor & index) == 0) {
        index <<= 1;
    }
    for (int i = 0; i < array.length; i++) {
        if ((array[i] & index) == 0) {
            num1[0] ^= array[i];
        } else {
            num2[0] ^= array[i];
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章