LeetCode通關:通過排序一次秒殺五道題,舒服!

三分惡發表於2021-09-07

刷題路線參考:https://github.com/chefyuan/algorithm-base

大家好,我是拿輸出部落格督促自己刷題的老三,前面學習了十大排序:萬字長文|十大基本排序,一次搞定!,接下來我們看看力扣上有沒有什麼能拿排序解決的題目吧!

排序基礎

簡單瞭解一下基本的排序——

基本排序分類:

排序分類

基本排序效能:

排序方法 時間複雜度(平均) 時間複雜度(最壞) 時間複雜度(最好) 空間複雜度 穩定性
氣泡排序 O(n²) O(n²) O(n) O(1) 穩定
選擇排序 O(n²) O(n²) O(n²) O(1) 不穩定
插入排序 O(n²) O(n²) O(n) O(1) 穩定
希爾排序 O(n^(1.3-2)) O(n²) O(n) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
快速排序 O(nlogn) O(n²) O(nlogn) O(nlogn) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
計數排序 O(n+k) O(n+k) O(n+k) O(n) 穩定
桶排序 O(n+k) O(n²) O(n) O(n+k) 穩定
基數排序 O(n*k) O(n*k) O(n*k) O(n+k) 穩定

更具體的可以檢視:萬字長文|十大基本排序,一次搞定!

好了,開始我們愉快的刷題之旅吧!

刷題現場

LeetCode912. 排序陣列

☕ 題目:912. 排序陣列 (https://leetcode-cn.com/problems/sort-an-array/)

❓ 難度:中等

? 描述:給你一個整數陣列 nums,請你將該陣列升序排列。

示例 1:

輸入:nums = [5,2,3,1]
輸出:[1,2,3,5]

示例 2:

輸入:nums = [5,1,1,2,0,0]
輸出:[0,0,1,1,2,5]

? 思路:

這道題如果用api,一行就搞定了——Arrays.sort(nums),那面試官的反應多半是,門在那邊,慢走不送。

所以,毫無疑問,我們要手撕排序了。

如果對排序演算法不太熟,可以上一個氣泡排序,但是這個明顯只能說中規中矩,所以,我們選擇:

手撕快排

關於快排,就不多講。

直接上程式碼:

class Solution {
    public int[] sortArray(int[] nums) {
       quickSort(nums,0,nums.length-1);
       return nums;
    }

    public void quickSort(int[] nums,int left, int right){
     //結束條件
      if(left>=right){
        return;
      }
      //分割槽
      int partitionIndex=partition(nums,left,right);
      //遞迴左分割槽
      quickSort(nums,left,partitionIndex-1);
      //遞迴右分割槽
      quickSort(nums,partitionIndex+1,right);
    }

    public int partition(int[] nums,int left,int right){
        //基準值
        int pivot=nums[left];
        //mark標記下標
        int mark=left;
        for(int i=left+1;i<=right;i++){
            if(nums[i]<pivot){
                //小於基準值,則mark後移,並交換位置
                mark++;
                int temp=nums[mark];
                nums[mark]=nums[i];
                nums[i]=temp;
            }
        }
        //把基準值放到mark的位置
        nums[left]=nums[mark];
        nums[mark]=pivot;
        return mark;
    }
}
  • ? 時間複雜度:快排時間複雜度O(nlogn)

有時間的可以把十大排序都在這道題練上一練。

LeetCode347. 前 K 個高頻元素

☕ 題目:347. 前 K 個高頻元素(https://leetcode-cn.com/problems/top-k-frequent-elements/)

❓ 難度:中等

? 描述:

給你一個整數陣列 nums,請你將該陣列升序排列。

示例 1:

輸入: nums = [1,1,1,2,2,3], k = 2
輸出: [1,2]

示例 2:

輸入: nums = [1], k = 1
輸出: [1]

提示:

1 <= nums.length <= 105
k 的取值範圍是 [1, 陣列中不相同的元素的個數]
題目資料保證答案唯一,換句話說,陣列中前 k 個高頻元素的集合是唯一的

進階:你所設計演算法的時間複雜度 必須 優於 O(n log n) ,其中 n 是陣列大小。

? 思路:

這道題第一思路是什麼呢?

統計元素出現頻率,從大到小排序,取前k個元素。

我們想挑戰一下進階要求,時間複雜度優於O(nlogn),所以熟悉的冒泡、快排之類的比較類排序都不可用,只能使用非比較類的三種排序方法:計數排序、桶排序、基數排序。

這裡我們選擇HashMap+桶排序的方式。

使用HashMap儲存元素出現頻率,使用桶排序來進行排序。

程式碼如下:

    public int[] topKFrequent(int[] nums, int k) {
        //使用HashMap儲存元素出現頻率
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        //桶
        List<Integer>[] buckets = new List[nums.length + 1];
        //往桶裡新增元素出現次數
        for (Integer key : map.keySet()) {
            //根據出現頻率決定元素入哪個桶
            int count = map.get(key);
            //初始化桶
            if (buckets[count] == null) buckets[count] = new ArrayList<>();
            //將元素存到桶中
            buckets[count].add(key);
        }
        //結果列表
        List<Integer> result = new ArrayList<>();
        //取倒數k個非空桶中的元素
        for (int i = buckets.length - 1; k > 0; i--) {
            if (buckets[i] != null) {
                //取出桶中的元素
                for (Integer num : buckets[i]) {
                    result.add(num);
                    k--;
                }
            }
        }
        //將列表中的元素賦給陣列
        int[] res = new int[result.size()];
        for (int i = 0; i < res.length; i++) {
            res[i] = result.get(i);
        }
        return res;
    }
  • ? 時間複雜度:這道題用了桶排序,時間複雜度O(n)。

劍指 Offer 45. 把陣列排成最小的數

☕ 題目:劍指 Offer 45. 把陣列排成最小的數 (https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/)

❓ 難度:中等

? 描述:

輸入一個非負整數陣列,把陣列裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。

示例 1:

輸入: [10,2]
輸出: "102"

示例 2:

輸入: [3,30,34,5,9]
輸出: "3033459"

? 思路:

稍微分析一下這道題,發現這道題其實也是一道排序題。

只要我們把陣列裡的元素按照某種規則進行排序。

現在的問題就是這個排序規則是什麼呢?

因為需要拼接字串,以[3,30]為例,“3”+“30”=“330”,“30”+"3"="303",330>303,那麼我們就可以說3大於30。

所以定義規則:

  • 若拼接字串 x+y>y+x ,則 x 大於y ;
  • 反之,若拼接字串 x+y<y+x ,則 x 小於 y ;

規則圖如下(來源參考[2):

圖片來源參考[2]

那麼,這道題我們就知道怎麼寫了。

用我們自定義的排序規則從小到大排序陣列。

排序方法我們選擇快排,所以這道題就是自定義排序+快排

程式碼如下:

    public String minNumber(int[] nums) {
        quickSort(nums, 0, nums.length - 1);
        //結果
        StringBuilder sb = new StringBuilder();
        for (int num : nums) {
            sb.append(String.valueOf(num));
        }
        return sb.toString();
    }

    //快排
    public void quickSort(int[] nums, int left, int right) {
        if (left >= right) return;
        int partionIndex = partion(nums, left, right);
        quickSort(nums, left, partionIndex - 1);
        quickSort(nums, partionIndex + 1, right);
    }

    public int partion(int[] nums, int left, int right) {
        int pivot = nums[left];
        int mark = left;
        for (int i = left + 1; i <= right; i++) {
            if (lessThan(nums[i], pivot)) {
                mark++;
                int temp = nums[mark];
                nums[mark] = nums[i];
                nums[i] = temp;
            }
        }
        nums[left] = nums[mark];
        nums[mark] = pivot;
        return mark;
    }

    //自定義大小比較規則
    public boolean lessThan(int x, int y) {
        String sx = String.valueOf(x), sy = String.valueOf(y);
        return (sx + sy).compareTo(sy + sx) < 0;
    }

寫的比較臃腫,但比較清晰。

有一種利用內建排序來實現的寫法,不太建議:

    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++){
            strs[i] = String.valueOf(nums[i]);
        }
        Arrays.sort(strs, (x,y) -> (x+y).compareTo(y+x));
        StringBuilder ans = new StringBuilder();
        for(String s : strs)
            ans.append(s);
        return ans.toString();
    }
  • ? 時間複雜度:O(nlogn)。

有一道題:179. 最大數 和這道題基本一樣。

劍指 Offer 51. 陣列中的逆序對

☕ 題目:劍指 Offer 51. 陣列中的逆序對 (https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)

❓ 難度:困難

? 描述:

在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個陣列,求出這個陣列中的逆序對的總數。

示例 1:

輸入: [7,5,6,4]
輸出: 5

?思路:

這一道題是困難,有沒有被嚇住?

其實這道題如果用歸併排序的思路去解決的話,就沒有想象中的那麼難。

歸併排序這裡就不講了。

解決這道題,我們只需要在歸併排序的基礎上,加上對逆序對的統計:

歸併+逆序對統計示意圖(圖片來源參考[3]):

歸併+逆序對統計(圖片來源參考[3])

現在的關鍵點是,歸併的過程如何計算逆序對個數?

我們可以看一下,合併的時候,l指向左子陣列2的位置,r指向右子陣列0的位置,num[l]>nums[r],因為子陣列是有序的,所以l後面幾個元素也都一定大於0,所以可以得出,此時逆序對數量=mid-l+1。

歸併計算逆序對

程式碼如下:

class Solution {
    //統計逆序對
    int count = 0;

    public int reversePairs(int[] nums) {
        mergeSort(nums, 0, nums.length - 1);
        return count;
    }

    //歸併排序
    public void mergeSort(int[] nums, int left, int right) {
        //結束
        if (left >= right) return;
        int mid = left + (right - left) / 2;
        //左半部分
        mergeSort(nums, left, mid);
        //右半部分
        mergeSort(nums, mid + 1, right);
        //合併
        merge(nums, left, mid, right);
    }

    //合併
    public void merge(int[] arr, int left, int mid, int right) {
        //臨時陣列
        int[] tempArr = new int[right - left + 1];
        //指向左右子陣列指標
        int l = left, r = mid + 1;
        int index = 0;
        //把左右子陣列較小元素放入到臨時陣列
        while (l <= mid && r <= right) {
            if (arr[l] <= arr[r]) {
                tempArr[index++] = arr[l++];
            } else {
                //增加一行,統計逆序對
                count += (mid - l + 1);
                tempArr[index++] = arr[r++];
            }
        }
        //將左子陣列剩餘的元素拷貝到臨時陣列
        while (l <= mid) {
            tempArr[index++] = arr[l++];
        }
        //將右邊子陣列剩餘的元素拷貝到臨時陣列
        while (r <= right) {
            tempArr[index++] = arr[r++];
        }
        //將臨時陣列的元素拷貝給原陣列
        for (int i = 0; i < tempArr.length; i++) {
            arr[i + left] = tempArr[i];
        }
    }
}
  • ? 時間複雜度:歸併排序時間複雜度O(nlogn)。

LeetCode147. 對連結串列進行插入排序

☕ 題目:劍指 Offer 51. 陣列中的逆序對 (https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)

❓ 難度:困難

? 描述:

對連結串列進行插入排序。

插入排序的動畫演示如上。從第一個元素開始,該連結串列可以被認為已經部分排序(用黑色表示)。
每次迭代時,從輸入資料中移除一個元素(用紅色表示),並原地將其插入到已排好序的連結串列中。

插入排序演算法:

  1. 插入排序是迭代的,每次只移動一個元素,直到所有元素可以形成一個有序的輸出列表。
  2. 每次迭代中,插入排序只從輸入資料中移除一個待排序的元素,找到它在序列中適當的位置,並將其插入。
  3. 重複直到所有輸入資料插入完為止。

示例 1:

輸入: 4->2->1->3
輸出: 1->2->3->4

示例 2:

輸入: -1->5->3->4->0
輸出: -1->0->3->4->5

? 思路:

這道題不只是插入排序,還涉及到連結串列的操作,關於連結串列,可以檢視:LeetCode通關:聽說連結串列是門檻,這就抬腳跨門而入

  • 關於插入排序:我們需要從未排序序列裡將元素插入到排序序列的合適位置

插入排序

  • 關於連結串列插入:連結串列插入是插入節點前驅節點改變後繼的一個操作,為了頭插也能統一,通常我們會加一個虛擬頭節點

連結串列插入

  • 所以,綜合起來,我們需要標記有序序列和無序序列的分界點,遍歷無序序列的時候,記錄前驅,當需要將無序序列插入到有序序列的時候,遍歷有序序列,找到插入位置,先刪除該節點,再插入

連結串列插入排序

程式碼如下:

    public ListNode insertionSortList(ListNode head) {
        if (head == null && head.next == null) {
            return head;
        }
        //虛擬頭節點
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        //記錄有序序列終點
        ListNode last = head;
        //遍歷無序序列
        ListNode after = head.next;
        while (after != null) {
            if (last.val <= after.val) {
                after = after.next;
                last = last.next;
                continue;
            }
            //遍歷有序序列,查詢插入位置
            ListNode prev = dummy;
            while (prev.next.val <= after.val) {
                prev = prev.next;
            }
            //找到插入位置
            //刪除無序序列節點
            last.next = after.next;
            //插入有序序列
            after.next = prev.next;
            prev.next = after;
            //繼續移動
            after=last.next;
        }
        return dummy.next;
    }
  • ? 時間複雜度:O(n²)。

總結

熟悉的順口溜總結:

總結

簡單的事情重複做,重複的事情認真做,認真的事情有創造性地做。

我是三分惡,一個追求實力,正在努力的程式設計師。

點贊關注不迷路,我們們下期見!



參考:

[1]. https://github.com/chefyuan/algorithm-base

[2]. https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/solution/mian-shi-ti-45-ba-shu-zu-pai-cheng-zui-xiao-de-s-4/

[3]. https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/jian-zhi-offer-51-shu-zu-zhong-de-ni-xu-pvn2h/

相關文章