算法 | 一周刷完《劍指Offer》 Day3:第27~37題

寫在前面

上一篇:算法 | 一周刷完《劍指Offer》 Day2:第17~26題
下一篇:算法 | 一周刷完《劍指Offer》 Day4:第38~49題


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+S2S2+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~49題

希望這篇文章對你有幫助~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容