大家好,我是拿輸出部落格督促自己刷題的老三,前面學習了十大排序:萬字長文|十大基本排序,一次搞定!,接下來我們看看力扣上有沒有什麼能拿排序解決的題目吧!
排序基礎
簡單瞭解一下基本的排序——
基本排序分類:
基本排序效能:
排序方法 | 時間複雜度(平均) | 時間複雜度(最壞) | 時間複雜度(最好) | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
氣泡排序 | 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):
那麼,這道題我們就知道怎麼寫了。
用我們自定義的排序規則從小到大排序陣列。
排序方法我們選擇快排,所以這道題就是自定義排序+快排。
程式碼如下:
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]):
現在的關鍵點是,歸併的過程如何計算逆序對個數?
我們可以看一下,合併的時候,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:
輸入: 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²)。
總結
熟悉的順口溜總結:
簡單的事情重複做,重複的事情認真做,認真的事情有創造性地做。
我是三分惡,一個追求實力,正在努力的程式設計師。
點贊
、關注
不迷路,我們們下期見!
參考: