目錄
分治算法即分而治之,就是把一個復雜的問題分解成兩個或多個相同或相似的子問題,再把子問題分解成更小的問題。。。直到最后子問題可以簡單地直接求解,原問題即子問題的合並。分治算法主要分為三個步驟:
- 分解:將問題划分成一系列子問題,子問題的形式和原問題一樣,只是規模更小
- 解決:遞歸地求出子問題。如果子問題的規模足夠小,即停止遞歸,直接求解
- 合並:步驟將子問題的解組合成原問題的解
(此例子引用連接如下:https://www.zhihu.com/search?type=content&q=%E5%88%86%E6%B2%BB%E7%AE%97%E6%B3%95)
有這樣一個經典的問題:有100枚硬幣,其中1枚重量與眾不同,是假幣,更輕一些。如果用天平秤,請問至少稱多少次一定能找到這枚假幣
加入我們用傳統的枚舉法,顯然至少需要比較50次
而假設我們采用分治法的話 ,流程如下:1. 將100硬幣分成3份,33,33,34。
2.稱量1、2份,若天平平衡,則假幣必在另外34枚中。若不平衡,假幣在輕的那33枚里。
3.將34枚分為11/11/12枚(或將33枚分成11*3)。
4.稱量兩組11枚的硬幣,若平衡,假幣在12枚里(或另外的11枚)若不平衡,假幣在輕的11里。
5.將11(或12)枚分成3/4/4(或4/4/4),稱量4/4,方法同上。
6.將剩下的3(或4)分為1/1/1(或1/1),稱量1/1,若平衡,則剩下的一枚是假幣,若不平衡,輕的是假幣。 若還剩4枚,出現1/1平衡,剩下2枚則稱量,顯然輕的是假幣。
這種方法只需要5次就能解決這個問題。
- 問題分析
解決此問題有兩種方法:
-
- 方法一:傳統暴力解決方法,需要遍歷整個數組,其時間復雜度為 O(n)
設置一個當前最大值和全局最大值,遍歷整個數組,當此時的最大值小於0時,則前面的都不要,否則,將下一個數相加即可
1 class Solution { 2 public: 3 int FindGreatestSumOfSubArray(vector<int> array) { 4 /* 5 要實現求得連續子數組的最大和,我們可以 6 */ 7 if(array.empty()) 8 return 0; 9 int cursum=0; 10 int maxsum=array[0]; 11 for(int i=0;i<array.size();++i) 12 { 13 if(cursum<=0) 14 cursum=array[i]; 15 else 16 { 17 cursum+=array[i]; 18 } 19 if(cursum>=maxsum) 20 maxsum=cursum; 21 } 22 return maxsum; 23 } 24 };
-
- 方法二:使用分治算法
要找到最大連續和的子數組,我們可以用分治算法,設最左邊為left,最右邊為right,中間節點為mid,則最大子數組可以分為以下三種情況:
1. 最大連續子數組都左邊子數組[left, mid]
2. 最大連續子數組都在右邊子數組[mid+1,right]
3. 最大連續子數組跨越了中點,一部分在左邊子數組中,一部分在右邊子數組中
最大連續子數組一定是三種情況中的最大值
1 class Solution { 2 /* 3 要找到連續最大子序和,我們可以用分治算法,設最左邊為left, 最右邊為right,中間節點為mid,則最大連續子數組可能情況為以下三種: 4 1. 最大連續子數組都在[left, mid]中 5 2. 最大連續子數組都在[mid+1, right]中 6 3. 最大連續子數組跨越了中點,因此left<=i<=mid<=j<=right 7 */ 8 //只需要在這三種情況中找出最大值即可 9 //首先是求出mid在子數組中間的情況 10 /* 11 我們的目的是找出mid在子數組中的子數組的和 12 */ 13 int INT_MIN=-2147483648; 14 public int find_max_cross_substr(int[] nums,int left,int mid,int right) 15 { 16 int maxleft=0,maxright=0; 17 int sum=0; 18 int leftsum=INT_MIN,rightsum=INT_MIN; 19 //首先找到左半邊子數組的最大值 20 for(int i=mid;i>=left;--i) 21 { 22 sum+=nums[i]; 23 leftsum=Math.max(sum,leftsum); 24 } 25 //然后找到右半邊子數組的最大值 26 sum=0; 27 for(int j=mid+1;j<=right;++j) 28 { 29 sum+=nums[j]; 30 rightsum=Math.max(sum,rightsum); 31 } 32 return (leftsum+rightsum); 33 } 34 //然后進行遞歸查找,比較三種情況,找出最大值 35 public int find_max_substr(int[]nums,int left,int right) 36 { 37 if(left==right) 38 return nums[left];//找到遞歸結束條件,結束遞歸 39 else 40 { 41 int mid=left+(right-left)/2; 42 int maxleftsum,maxrightsum,maxcrosssum; 43 maxleftsum=find_max_substr(nums,left,mid); 44 maxrightsum=find_max_substr(nums,mid+1,right); 45 maxcrosssum=find_max_cross_substr(nums,left,mid,right); 46 return Math.max(Math.max(maxleftsum,maxrightsum),maxcrosssum); 47 } 48 49 } 50 public int maxSubArray(int[] nums) { 51 return find_max_substr(nums,0,nums.length-1); 52 } 53 }
1 class Solution { 2 /* 3 要找到連續最大子序和,我們可以用分治算法,設最左邊為left, 最右邊為right,中間節點為mid,則最大連續子數組可能情況為以下三種: 4 1. 最大連續子數組都在[left, mid]中 5 2. 最大連續子數組都在[mid+1, right]中 6 3. 最大連續子數組跨越了中點,因此left<=i<=mid<=j<=right 7 */ 8 //只需要在這三種情況中找出最大值即可 9 //首先是求出mid在子數組中間的情況 10 /* 11 我們的目的是找出mid在子數組中的子數組的和 12 */ 13 int INT_MIN=-2147483648; 14 public int find_max_cross_substr(int[] nums,int left,int mid,int right) 15 { 16 int maxleft=0,maxright=0; 17 int sum=0; 18 int leftsum=INT_MIN,rightsum=INT_MIN; 19 //首先找到左半邊子數組的最大值 20 for(int i=mid;i>=left;--i) 21 { 22 sum+=nums[i]; 23 leftsum=Math.max(sum,leftsum); 24 } 25 //然后找到右半邊子數組的最大值 26 sum=0; 27 for(int j=mid+1;j<=right;++j) 28 { 29 sum+=nums[j]; 30 rightsum=Math.max(sum,rightsum); 31 } 32 return (leftsum+rightsum); 33 } 34 //然后進行遞歸查找,比較三種情況,找出最大值 35 public int find_max_substr(int[]nums,int left,int right) 36 { 37 if(left==right) 38 return nums[left];//找到遞歸結束條件,結束遞歸 39 else 40 { 41 int mid=left+(right-left)/2; 42 int maxleftsum,maxrightsum,maxcrosssum; 43 maxleftsum=find_max_substr(nums,left,mid); 44 maxrightsum=find_max_substr(nums,mid+1,right); 45 maxcrosssum=find_max_cross_substr(nums,left,mid,right); 46 return Math.max(Math.max(maxleftsum,maxrightsum),maxcrosssum); 47 } 48 49 } 50 public int maxSubArray(int[] nums) { 51 return find_max_substr(nums,0,nums.length-1); 52 } 53 }
2. 合並兩個排序的鏈表
- 問題分析
(1) 使用while循環。思路:將鏈表B合並到鏈表A中。循環鏈表B,需要創建兩個指針,一個指向當前的節點,另一個指向當前的下一個結點。
(2) 分治算法+遞歸。思路:將兩個鏈表中,l1指針和l2指針指向的節點,比較大小,然后遞歸即可,注意需要添加遞歸結束的代碼
- 代碼參考
1 /** 2 * Definition for singly-linked list. 3 * public class ListNode { 4 * int val; 5 * ListNode next; 6 * ListNode(int x) { val = x; } 7 * } 8 */ 9 class Solution { 10 public ListNode mergeTwoLists(ListNode l1, ListNode l2) { 11 ListNode head=null; 12 if(l1==null) 13 return l2; 14 else if(l2==null) 15 return l1; 16 else 17 { 18 if(l1.val<l2.val) 19 { 20 head=l1; 21 head.next=mergeTwoLists(l1.next,l2); 22 } 23 else 24 { 25 head=l2; 26 head.next=mergeTwoLists(l1,l2.next); 27 } 28 } 29 return head; 30 } 31 }
- 問題分析
數組中出現次數超過一半的數字特征如下:數字中出現次數超過一半的數字出現總次數大於其余數字出現次數的總數
首先我們可以設置一個出現次數times,初始值為0
若當前數組的數等於上一次出現的次數,則times++
否則,times--
- 代碼參考
1 class Solution { 2 public int majorityElement(int[] nums) { 3 //如果一個數字出現的次數是數組長度的一半,則這個數組滿足以下要求 4 /* 5 首先,這個數字出現的次數比其他數字出現的次數加起來還多 6 我們可以設置一個出現次數times,初始化為0 7 若當前數組的數等於上一個出現的次數,則times++ 8 否則times-- 9 */ 10 if(nums.length==0) 11 return 0; 12 int times=0; 13 int result=nums[0]; 14 for(int i=0;i<nums.length;++i) 15 { 16 if(times==0) 17 result=nums[i]; 18 if(result==nums[i]) 19 ++times; 20 else 21 --times; 22 } 23 int count=0; 24 for(int i=0;i<nums.length;++i) 25 { 26 if(nums[i]==result) 27 ++count; 28 } 29 if(count*2>nums.length) 30 return result; 31 else 32 return 0; 33 } 34 }
- 題目描述
- 方法一(最大堆實現)
這是一道典型的TopK問題,比較直觀的方法是利用堆數據結構來輔助得到最小的k個數。堆的性質是每次可以找出最大或者最小的元素。我們可以使用一個大小為k的最大堆(大堆頂),將數組中的元素依次入堆,當堆的大小超過k時,便將多出的元素從堆頂彈出。如果數組中待插入的元素與堆頂元素進行比較,若待插入元素小於堆頂元素,則將堆頂元素彈出后將待插入元素插入堆
這樣,由於每次從堆頂彈出的數都是堆中最大的,最小的k個元素一定會留在堆中。這樣,把數組中的元素全部入堆后,堆中剩下的元素就是最大的k個數了。
- 代碼參考
1 class Solution { 2 public int[] getLeastNumbers(int[] arr, int k) { 3 /* 4 可以使用一個大根堆實時維護數組的前k個小值 5 首先將k個數插入大根堆中 6 然后從k+1個數開始遍歷 7 如果當前遍歷到的數比大根堆的堆頂的數要小,就把堆頂的數彈出,再插入當前遍歷到額數 8 最后將大根堆的數存入數組返回即可 9 */ 10 if(k==0) 11 return new int[0]; 12 //使用一個最大堆實現TopK問題 13 //Java的priorityQueue默認是小堆頂,添加comparator參數使其變成大堆頂 14 Queue<Integer> pq=new PriorityQueue<>(k,(i1,i2)->Integer.compare(i2,i1)); 15 16 //首先把數組中的前k個數字入堆 17 for(int num:arr) 18 { 19 if(pq.size()<k) 20 pq.offer(num); 21 else if(num<pq.peek()) 22 { 23 pq.poll(); 24 pq.offer(num); 25 } 26 } 27 //返回堆中的元素 28 int []res=new int[pq.size()]; 29 int idx=0; 30 for(int num:pq) 31 { 32 res[idx++]=num; 33 } 34 return res; 35 } 36 }
- 方法二:快速排序思想(分治算法)
可以使用快速排序思想來做,其是分治的思想
快速排序中有一步很重要的操作是partition(划分),從數組中隨機選取一個樞紐元素v,然后原地移動數組中的元素,使得v小的元素在v的左邊,比v大的元素在v的右邊,如下圖所示:
我們的目的是尋找最小的k個數。假設經過一次partition操作,樞紐元素位於下標m,也就是說,左側的數組有m個元素,是原數組中最小的m個數,那么:
-
- 若k=m,我們就找到了最小的k個數,就是左側的數組
- 若k<m,則最小的k個數一定在左邊數組,我們只需要對左側數組遞歸即可
- 若k>m,則左側數組中的m個數都屬於最小的k個數,我們還需要在右側數組中尋找最小的k-m個數,對右側數組遞歸地partition即可
- 代碼參考
1 class Solution { 2 /* 3 方法二,可以利用快速排序的思想 4 快速排序中有一個很重要的一步就是partition,從數組中隨機選取一個元素v,然后原地移動數組中的元素,使得比v小的元素在v的左邊,比v大的元素在v的右邊 5 我們的目的是找到最小的k個數,假設經過一次partition操作,樞紐元素位於下標m, 6 若k==m,則找到最小的k個元素,即左側的數組 7 若k<m,則最小的k個數一定在左側數組中,只需要對左側數組中尋找最小的k-m個數即可 8 若k>m,左側數組中的m個數都屬於最小的k個數,我們還需要右側數組中尋找最小的k-m個數,對右側數組遞歸的partition即可 9 */ 10 public int[] getLeastNumbers(int[] arr, int k) { 11 if(k==0) 12 return new int[0]; 13 else if(arr.length<=k) 14 return arr; 15 //原地不斷划分數組 16 partitionArray(arr,0,arr.length-1,k); 17 //數組的前k個數此時就是最小的k個數,將其存入結果 18 int[] res=new int[k]; 19 for(int i=0;i<k;++i) 20 { 21 res[i]=arr[i]; 22 } 23 return res; 24 25 } 26 int partition(int[] arr,int low,int high) 27 { 28 int i=low; 29 int j=high+1; 30 int pivot=arr[low]; 31 while(true) 32 { 33 //左邊:小而移動,大而賦值 34 while(arr[++i]<pivot) 35 { 36 if(i==high) 37 break; 38 } 39 //右邊:大而移動,小而賦值 40 while(arr[--j]>pivot) 41 { 42 if(j==low) 43 break; 44 } 45 if(i>=j) 46 break; 47 swap(arr,i,j); 48 } 49 swap(arr,low,j); 50 return j; 51 } 52 void swap(int[]arr,int i,int j) 53 { 54 int temp=arr[i]; 55 arr[i]=arr[j]; 56 arr[j]=temp; 57 } 58 void partitionArray(int [] arr,int low,int high,int k) 59 { 60 //實現第一次partition 61 int m=partition(arr,low,high); 62 if(m==k) 63 //剛好找到最小的k個數 64 return; 65 else if(k<m) 66 { 67 //最小的k個數一定在前m個數中,遞歸划分 68 partitionArray(arr,low,m-1,k); 69 } 70 else 71 { 72 //在右側數組中尋找最小的k-m個數 73 partitionArray(arr,m+1,high,k); 74 } 75 } 76 }