LC T668筆記 【涉及知識:二分查詢、第K小數、BFPRT演算法】
【以下內容僅為本人在做題學習中的所感所想,本人水平有限目前尚處學習階段,如有錯誤及不妥之處還請各位大佬指正,請諒解,謝謝!】
!!!觀前提醒!!!
【本文篇幅較大,如有興趣建議分段閱讀】
有關二分查詢
作用:在有序集合中快速查詢目標值
適用性:
1. 只能查詢有序的資料集
順序儲存的資料結果就是陣列了,也就是二分查詢只能從陣列中查詢,而不能查詢鏈式儲存的資料集,比如查詢連結串列中的數,就不能用二分查詢。
2. 針對的是靜態有序資料集
二分查詢適合那種不經常變動的資料集合。如果經常插入、刪除的資料集,每次插入和刪除都要保證集合資料的有序,維護動態資料有序的成本很高。所以二分查詢適合從有序的不經常變動的資料集合中查詢。適合資料集合已經排好序,但是需要經常查詢的場景。
3. 不適合資料量太大或者太小的場景
因為二分查詢需要依賴陣列這種資料結構,而陣列要求連續的記憶體空間,其需要把所有資料全部讀入記憶體中,因此資料量太大的,對記憶體要求比較高。如果資料量只有幾十個,那麼不論是使用二分查詢還是順序遍歷,查詢效率都差不多。
有關二分查詢的邊界問題
“思路很簡單,細節是魔鬼”
二分的幾個常用情景:尋找一個數、尋找左側邊界、尋找右側邊界
以下是二分查詢的基本框架:
1 public int BinarySearch(int[] nums, int target) { 2 int left = 0, right = ...; 3 while(...) { 4 int mid = left + ((right - left) >> 1); 5 if (nums[mid] == target) { 6 ... 7 } else if (nums[mid] < target) { 8 left = ... 9 } else if (nums[mid] > target) { 10 right = ... 11 } 12 } 13 return ...; 14 }
分析二分查詢的一個技巧是:不要出現 else,而是把所有情況用 else if 寫清楚,這樣可以清楚地展現所有細節。
(一) 尋找一個數
1 public int BinarySearch(int[] nums, int target) { 2 int left = 0; 3 int right = nums.length - 1; //【1】 4 5 while(left <= right) { //【2】 6 int mid = left + ((right - left) >> 1); 7 if(nums[mid] == target) 8 return mid; 9 else if (nums[mid] < target) 10 left = mid + 1; //【3】 11 else if (nums[mid] > target) 12 right = mid - 1; //【4】 13 } 14 return -1; 15 }
1. while中的迴圈條件
迴圈條件由搜尋區間的結構確定,當找到目標值後,返回即可;
若沒找到則需考慮終止情況。此處的搜尋區間的結構是兩端閉區間。當left == right時,表示區間[left, right],此時區間內仍有一個數值未被搜尋,若此時結束迴圈,可能錯過對目標值的匹配,因此需要繼續查詢,則終止條件應當是left > right時,此時搜尋區間為空。所以此處while中應當為“<=”。
如果要使用小於號,則在結尾加一句判斷即可。
1 return nums[left] == target ? left : -1;
2. left與right的加加減減
邊界的加減也由搜尋區間的結構確定。在[left, right]中mid被檢測後,需要據mid將其劃分為兩個區間,若mid位置上的值不等於target,則不用再考慮mid。因為邊界均可取到,所以搜尋區間因改為[left, mid – 1]或[mid + 1, right]
3. 缺點
當資料中重複出現目標元素,則返回的是在重複序列中中間位置的索引,並不能得到其左側或右側邊界。如{1, 2, 2, 2, 3, 5},target = 2,此時返回索引為2,但其邊界為[1, 3]
(二) 尋找左側邊界
1 public int LeftBound(int[] nums, int target) { 2 if (nums.length == 0) return -1; 3 int left = 0; 4 int right = nums.length; //【1】 5 6 while (left < right) { //【2】 7 int mid = left + ((left + right) >> 1); 8 if (nums[mid] == target) { 9 right = mid; //【3】 10 } else if (nums[mid] < target) { 11 left = mid + 1; 12 } else if (nums[mid] > target) { 13 right = mid; //【4】 14 } 15 } 16 return left; 17 }
1. while中的迴圈條件
同理,此處的搜尋區間為左閉右開型,當left == right時,表示區間[left, right),此時的區間已經為空,故可以終止。
注:這裡解釋一下為何上面用兩端閉區間,而這裡用左開後閉區間。因為這樣的寫法比較普遍,不這麼寫也可以,後文將會展示三種寫法(兩端閉,左開右閉,左閉右開)。
2. left與right的加減
因為此處是左閉右開區間,在[left, right)中mid被檢測後,需要據mid將其劃分為兩個區間,[left, mid)和[mid + 1, right)。為了保證區間結構不變,所以right應變為mid,left應變為mid + 1
3. 有關結尾的返回值
返回值表示目標值在序列中的左側邊界,等價於小於目標值的元素個數。分析可知left的取值範圍是[0, nums.Length],所以當left == nums.Length時,說明沒有一個元素小於target,即target在該序列中不存在,返回-1即可。(當然,最終的返回值也可以是right,因為終止條件是left == right)
1 if (left == nums.length) return -1; 2 return nums[left] == target ? left : -1;
4. 該演算法的核心,即為何可以查詢左側邊界
1 if (nums[mid] == target) 2 right = mid;
當nums[mid] == target時,因為資料有序,說明mid左側可能存在target,所以應縮小上界,不斷向左收縮。
5. 統一格式,將while迴圈加入等號
據原理,只需將right初值設為nums.Length – 1;right的變化改為mid – 1即可。
1 public int LeftBound(int[] nums, int target) { 2 int left = 0, right = nums.length - 1; 3 while (left <= right) { 4 int mid = left + (right - left) / 2; 5 if (nums[mid] == target) { 6 right = mid - 1; 7 left = mid + 1; 8 } else if (nums[mid] > target) { 9 right = mid - 1; 10 } else if (nums[mid] < target) { 11 left = mid + 1; 12 } 13 } 14 if (left >= nums.length || nums[left] != target) 15 return -1; 16 return left; 17 }
(三) 尋找右邊界
1 public int RightBound(int[] nums, int target) { 2 if (nums.length == 0) return -1; 3 int left = 0, right = nums.length; 4 while (left < right) { 5 int mid = left + ((left + right) >> 1); 6 if (nums[mid] == target) { 7 left = mid + 1; //【1】 8 } else if (nums[mid] < target) { 9 left = mid + 1; 10 } else if (nums[mid] > target) { 11 right = mid; 12 } 13 } 14 return left - 1; //【2】 15 }
1. left與right的加減
因為此處是左閉右開區間,在[left, right)中mid被檢測後,需要據mid將其劃分為兩個區間,[left, mid)和[mid + 1, right) 。為了保證區間結構不變,所以right應變為mid,left應變為mid + 1
2. 有關最後返回值
因為對left的更新為mid + 1,結束時會產生以下結果:
[注:上圖來源於搜尋引擎查詢結果】
所以需返回left – 1(也可返回right - 1)。
同理,當left == 0時,說明沒有一個元素大於target,即target在該序列中不存在,返回-1即可。
1 if (left == 0) return -1; 2 return nums[left-1] == target ? (left-1) : -1;
3. 統一格式
1 public int RightBound(int[] nums, int target) { 2 int left = 0, right = nums.length - 1; 3 while (left <= right) { 4 int mid = left + ((right - left) >> 1); 5 if (nums[mid] == target) { 6 left = mid + 1; 7 } else if (nums[mid] > target) { 8 right = mid - 1; 9 } else if (nums[mid] < target) { 10 left = mid + 1; 11 } 12 } 13 if (right < 0 || nums[right] != target) 14 return -1; 15 return right; 16 }
小結
1. 寫二分查詢時,儘量不要出現 else,將所有情況列出來便於分析。
2. 注意搜尋區間形式和 while 的終止條件,若存在漏掉的元素,最後特判。
3. 如需定義左閉右開的搜尋區間,搜尋左右邊界,只要在 nums[mid] == target 時做修改即可,搜尋右側時需要減一。
4. 如果將搜尋區間全都統一成兩端閉,只要修改 nums[mid] == target 條件處的程式碼和返回的邏輯即可。
從一維二分談起
二分法,用於在集合中查詢某些符合要求的元素,可以將時間複雜度降低至對數級。使用二分法的前提是查詢序列的有序性,主要思想是從序列中間位置開始,根據當前的中間值與目標值的大小關係,修改區間端點,確定目標值所在區間。
- 題一:搜尋旋轉排序陣列 33. 搜尋旋轉排序陣列 - 力扣(LeetCode)
題意:在半有序的結合中查詢目標元素的索引值
思想:選定中點,比較中點值來更改區間,但需要先判斷當前所查詢的區間是否為有序區間,否則不能使用二分法
1 //C# Version 2 3 public class Solution { 4 public int Search(int[] nums, int target) { 5 int n = nums.Length; 6 if(n == 0) return -1; 7 if(n == 1) return nums[0] == target ? 0 : -1; 8 9 int left = 0, right = n - 1; 10 while(left <= right) { 11 int mid = left + ((right - left) >> 1); 12 if(target == nums[mid]) return mid; 13 if(nums[0] <= nums[mid]) { 14 if(nums[0] <= target && target < nums[mid]) right = mid - 1; 15 else left = mid + 1; 16 } 17 else if(nums[0] > nums[mid]){ 18 if(nums[mid] < target && target <= nums[n - 1]) left = mid + 1; 19 else right = mid - 1; 20 } 21 } 22 return -1; 23 } 24 }
1 //C++ Version 2 3 class Solution { 4 public: 5 int search(vector<int>& nums, int target) { 6 int n = (int)nums.size(); 7 if(n == 0) return -1; 8 if(n == 1) return nums[0] == target ? 0 : -1; 9 10 int left = 0, right = n - 1; 11 while(left <= right) { 12 int mid = left + ((right - left) >> 1); 13 if(target == nums[mid]) return mid; 14 if(nums[0] <= nums[mid]) { 15 if(nums[0] <= target && target < nums[mid]) right = mid - 1; 16 else left = mid + 1; 17 } 18 else if(nums[0] > nums[mid]){ 19 if(nums[mid] < target && target <= nums[n - 1]) left = mid + 1; 20 else right = mid - 1; 21 } 22 } 23 return -1; 24 } 25 };
- 題二:長度最小的子陣列 209. 長度最小的子陣列 - 力扣(LeetCode)
題意:找出陣列中滿足其和大於等於目標值的長度最小的連續子序列
思想:要判斷連續區間內的和,就先求出原陣列的字首和,因為題保證了陣列中每個元素都為正,所以字首和一定是遞增的,保證了二分的正確性。
得到字首和之後,對於每個開始下標 i,可通過二分查詢得到大於或等於i的最小下標 bound,使得 sum[bound] - sum [i−1] ≥ target,並更新子陣列的最小長度,此時子陣列的長度是bound - i + 1。
【注:此解法非最優解】
1 //C# Version 2 3 public class Solution { 4 public int MinSubArrayLen(int target, int[] nums) { 5 int n = nums.Length; 6 if (n == 0) return 0; 7 int ans = int.MaxValue; 8 int[] sums = new int[n + 1]; 9 for (int i = 1; i <= n; ++i) 10 sums[i] = sums[i - 1] + nums[i - 1]; 11 for (int i = 1; i <= n; ++i) { 12 int s = target + sums[i - 1]; 13 int bound = LowerBound(sums, i, n - 1, s); 14 if (bound != -1) 15 ans = Math.Min(ans, bound - i + 1); 16 } 17 return ans == int.MaxValue ? 0 : ans; 18 } 19 private int LowerBound(int[] nums, int left, int right, int s) { 20 while (left <= right) { 21 int mid = left + ((right - left) >> 1); 22 if (nums[mid] < s) left = mid + 1; 23 else right = mid - 1; 24 } 25 return (nums[left] >= s) ? left : -1; 26 } 27 }
1 //C++ Version 2 3 class Solution { 4 public: 5 int minSubArrayLen(int s, vector<int>& nums) { 6 int n = nums.size(); 7 if (n == 0) return 0; 8 int ans = INT_MAX; 9 vector<int> sums(n + 1, 0); 10 for (int i = 1; i <= n; i++) 11 sums[i] = sums[i - 1] + nums[i - 1]; 12 for (int i = 1; i <= n; i++) { 13 int target = s + sums[i - 1]; 14 auto bound = lower_bound(sums.begin(), sums.end(), target); 15 if (bound != sums.end()) 16 ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1))); 17 } 18 return ans == INT_MAX ? 0 : ans; 19 } 20 };
- 題三:尋找峰值 162. 尋找峰值 - 力扣(LeetCode)
題意:找到陣列中某個峰值元素的索引(且nums[-1]與nums[len] = 負無窮)
思想:首先思考如何判斷峰值所在區間。
假設mid < mid + 1
- 對於mid – 1,無論是mid – 1 > mid還是mid – 1 > mid均不能得到mid是峰值;
- 對於mid + 2,有兩種情況:若mid + 2 < mid + 1則峰值為mid + 1;若mid + 2 > mid + 1,繼續後推,由於邊界後的值為-∞,那麼一定可以得到最後一個值為峰值。
綜上:峰值一定在較大的一部分。
1 //C# Version 2 3 public class Solution { 4 public int FindPeakElement(int[] nums) { 5 int left = 0, right = nums.Length - 1; 6 while(left < right) 7 { 8 int mid = left + (right - left) / 2; 9 if(nums[mid] > nums[mid + 1]) right = mid; 10 else left = mid + 1; 11 } 12 return left; 13 } 14 }
1 //C++ Version 2 3 int findPeakElement(vector<int>& nums) { 4 int left = 0, right = nums.size() - 1; 5 for (; left < right; ) { 6 int mid = left + (right - left) / 2; 7 if (nums[mid] > nums[mid + 1]) { 8 right = mid; 9 } else { 10 left = mid + 1; 11 } 12 } 13 return left; 14 }
小結
一維二分思想和操作較為簡單,具體步驟為:
1. 確定並構建查詢物件。即是查詢元素,還是查詢和、差等,構建出用於查詢的序列,如:字首和。
2. 判斷二分後目標值可能的所在區間。一般是通過中值和目標值的比較更改區間,特殊地(如峰值尋找)需要運用一定數學知識進行判斷。
有關二維二分
二維本質上可以看作是一維的疊加,某些簡單的情況下,可以一維一維的查詢。也可以從定義出發,從中點開始進行區間更改。當然,二維二分也有一些常見的變式,如從一個端點、對角線兩個端點出發等。
- 題一:搜尋二維矩陣 74. 搜尋二維矩陣 - 力扣(LeetCode)
題意:在二維矩陣中查詢某個值是否存在。
思想:可以將二維陣列劃分為一維陣列,一行一行或一列一列進行判斷。可以對矩陣的第一列的元素二分查詢,找到最後一個不大於目標值的元素,然後在該元素所在行,進行二分查詢目標值是否存在。
1 //C# Version 2 3 class Solution { 4 public bool SearchMatrix(int[][] matrix, int target) { 5 int rowIndex = BinarySearchFirstColumn(matrix, target); 6 if (rowIndex < 0) return false; 7 return BinarySearchRow(matrix[rowIndex], target); 8 } 9 10 private int BinarySearchFirstColumn(int[][] matrix, int target) { 11 int low = -1, high = matrix.Length - 1; 12 while (low < high) { 13 int mid = (high - low + 1) / 2 + low; 14 if (matrix[mid][0] <= target) low = mid; 15 else high = mid - 1; 16 } 17 return low; 18 } 19 20 private bool BinarySearchRow(int[] row, int target) { 21 int low = 0, high = row.Length - 1; 22 while (low <= high) { 23 int mid = (high - low) / 2 + low; 24 if (row[mid] == target) return true; 25 else if (row[mid] > target) high = mid - 1; 26 else low = mid + 1; 27 } 28 return false; 29 } 30 }
也可以從定義出發,從中間點開始進行判斷。
1 //C# Version 2 3 public class Solution { 4 public bool SearchMatrix(int[][] matrix, int target) { 5 int m = matrix.Length, n = matrix[0].Length; 6 int low = 0, high = m * n - 1; 7 while (low <= high) { 8 int mid = low + ((high - low) >> 1); 9 int x = matrix[mid / n][mid % n]; 10 if (x < target) low = mid + 1; 11 else if (x > target) high = mid - 1; 12 else return true; 13 } 14 return false; 15 } 16 }
注意到每行的第一個整數大於前一行的最後一個整數。因此,把每一行拼接到前一行可以得到一個遞增序列,所以可以從右上角開始進行判斷。
1 //C# Version 2 3 public class Solution { 4 public bool SearchMatrix(int[][] matrix, int target) { 5 int n = matrix.Length; 6 if(n == 0) return false; 7 int row = 0, col = matrix[0].Length - 1; 8 while(row < n && col >= 0) 9 { 10 if(matrix[row][col] < target) row++; 11 else if(matrix[row][col] >target) col--; 12 else return true; 13 } 14 return false; 15 } 16 }
1 //C++ Version 2 3 class Solution { 4 public: 5 bool searchMatrix(vector<vector<int>>& matrix, int target) { 6 int row = matrix.size(), col = matrix[0].size(); 7 for(int i = 0, j = col-1; i < row && j >= 0;) { 8 if(matrix[i][j] == target) 9 return true; 10 else if(matrix[i][j] > target) 11 j--; 12 else if(matrix[i][j] < target) 13 i++; 14 } 15 return false; 16 } 17 };
- 題二:有序矩陣中的第K小的元素 378. 有序矩陣中第 K 小的元素 - 力扣(LeetCode)
題意:在矩陣中找到第K小數
思想:可以從定義出發,從中間點開始進行判斷。關鍵是統計對於當前數mid,有多少個比它小的數。
若每行的第一個整數大於前一行的最後一個整數,則cnt = i * n + j。但本題不滿足該條件,則需要尋找一個參照值,通過迴圈,統計小於等於當前值的元素數。觀察四個邊角,左上角的元素最小,右下角的元素最大,而左下角和右上角的元素大小與mid相比是未定的,不妨取二者其一作為參照值。
在此,取左下角的值為參照值。
1 //C# Version 2 3 public class Solution { 4 public int KthSmallest(int[][] matrix, int k) { 5 int n = matrix.Length; 6 int left = matrix[0][0], right = matrix[n - 1][n - 1]; 7 while(left < right) { 8 int mid = left + ((right - left) >> 1); 9 if(Check(matrix, mid, k, n)) right = mid; 10 else left = mid + 1; 11 } 12 return left; 13 } 14 private bool Check(int[][] matrix, int mid, int k, int n) { 15 int cnt = 0; 16 int i = n - 1, j = 0; 17 while(i >= 0 && j < n) { 18 if(matrix[i][j] > mid) i--; 19 else { 20 cnt += i + 1; 21 j++; 22 } 23 } 24 return cnt >= k; 25 } 26 }
- 題三 乘法表中第K小數 668. 乘法表中第k小的數 - 力扣(LeetCode)
本題與上題類似,只是在計數上有變化。
1 /C# Version 2 3 public class Solution { 4 public int FindKthNumber(int m, int n, int k) { 5 int left = 1, right = m * n; 6 while(left < right) { 7 int mid = left + ((right - left) >> 1); 8 if(CheckCnt(mid, k, m, n)) right = mid; 9 else left = mid + 1; 10 } 11 return left; 12 } 13 private bool CheckCnt(int mid, int k, int m, int n) { 14 int cnt = 0; 15 for(int i = 1; i <= m; i++) cnt += Math.Min(mid / i, n); 16 return cnt >= k; 17 } 18 }
小結
二維二分通常從邊角出發,通常以邊角值為參照值,進行區間的更新。其本質依舊是比大小,改區間。
有關第K小數
在此介紹一種演算法:中位數的中位數算(BFPRT),該演算法主要解決TOP-K問題。
有一個經典的問題,“從長度為N的無序陣列中找出前k大的數”。TOP-K問題的最簡單解法為快速排序後取第K大的數,但快速排序可能會達到最壞情況時間複雜度O(n2),且會對無用的資料進行排序操作(歸併除外)。而該演算法的主要優化是,修改快速排序選擇主元的方法,優化最壞時間複雜度。
對於快速排序,一般選擇中間位置的元素作為參照值,將小的數移到參照值左邊,大的數移到右邊,此時對於中間位置的該值,即為序列中第n/2小的數。
那麼,是否可以用類似的方法,通過一次O(n)的操作找出第k小數呢?
該演算法通過“隨機選擇”實現了這個操作,其思想與快排類似,僅僅改變了對參照值的選取。
具體流程:
1.將n個元素劃為 n/5 組,每組5個,至多隻有一組由 n%5 個元素組成。
2.尋找每一個組的中位數(可以用插排)。
3.對步驟2選出的 n/5 箇中位數,重複步驟1和步驟2,遞迴下去,直到剩下一個數字。
4.最終剩下的數字近似為序列的中位數pivot,把小於等於它的數放左邊,大於的數放右邊。
5.判斷pivot的位置與k的大小,如果pivot > k,則在[0, pivot – 1]內尋找第k小數;反之在[pivot + 1, n - 1]內尋找 k – pivot 小的數。
注意下面兩種分治的思想:
1.分治法O(nlogn):大問題分解為小問題,小問題都要遞迴各個分支,例如:快速排序。
2.減治法O(n):大問題分解為小問題,小問題只要遞迴一個分支,例如:二分查詢,隨機選擇。
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 int InsertSort(int array[], int left, int right); 5 int GetPivotIndex(int array[], int left, int right); 6 int Partition(int array[], int left, int right, int pivot_index); 7 int BFPRT(int array[], int left, int right, int k); 8 9 ///劃分 10 int Partition(int arr[], int left, int right, int pivot_index) { 11 swap(arr[pivot_index], arr[right]); // 把主元放置於末尾 12 13 int partition_index = left; // 跟蹤劃分的分界線 14 for (int i = left; i < right; i++) 15 if (arr[i] < arr[right]) 16 swap(arr[partition_index++], arr[i]); // 比pivot小的都放在左側 17 18 swap(arr[partition_index], arr[right]); // 最後把pivot換回來 19 return partition_index; 20 } 21 22 ///返回第 k 小數的下標 23 int BFPRT(int arr[], int left, int right, int k) { 24 int pivot_index = GetPivotIndex(arr, left, right); // 得到中位數的中位數下標 25 int partition_index = Partition(arr, left, right, pivot_index); // 進行劃分,返回劃分邊界 26 int num = partition_index - left + 1; 27 28 if (num == k) 29 return partition_index; 30 else if (num > k) 31 return BFPRT(arr, left, partition_index - 1, k); 32 else 33 return BFPRT(arr, partition_index + 1, right, k - num); 34 } 35 36 ///返回 [left, right]的中位數。 37 int Insertion(int arr[], int left, int right) { 38 int temp, j; 39 for (int i = left + 1; i <= right; i++) { 40 temp = arr[i]; 41 j = i - 1; 42 while (j >= left && arr[j] > temp) { 43 arr[j + 1] = arr[j]; 44 j--; 45 } 46 arr[j + 1] = temp; 47 } 48 return left + ((right - left) >> 1); 49 } 50 51 ///陣列每五個元素作為一組,並計算每組的中位數,最後返回這些中位數的中位數下標 52 ///末尾返回語句最後一個引數多加 1 的作用是向上取整,可以始終保持 k 大於 0。 53 int GetPivotIndex(int arr[], int left, int right) { 54 if (right - left < 5) 55 return Insertion(arr, left, right); 56 int sub_right = left - 1; 57 58 // 每五個作為一組,求出中位數,並把這些中位數全部依次移動到陣列左邊 59 for (int i = left; i + 4 <= right; i += 5) { 60 int index = Insertion(arr, i, i + 4); 61 swap(arr[++sub_right], arr[index]); 62 } 63 64 // 利用 BFPRT 得到這些中位數的中位數下標 65 return BFPRT(arr, left, sub_right, ((sub_right - left + 1) >> 1) + 1); 66 } 67 68 int main() { 69 ios::sync_with_stdio(false); 70 int k = 8; // 1 <= k <= array.size 71 int nums[20] = { 12, 9, 7, 1, 13, 9, 15, 0, 26, 2, 17, 5, 14, 31, 6, 18, 22, 7, 19, 41 }; 72 73 cout << "The Source Data:"; 74 for (int i = 0; i < 20; i++) 75 cout << nums[i] << " "; 76 cout << endl; 77 78 // 因為是以 k 為劃分,所以還可以求出第 k 小值 79 cout << "The Kth smallest number:" << nums[BFPRT(nums, 0, 19, k)] << endl; 80 81 cout << "After Processing:"; 82 for (int i = 0; i < 20; i++) 83 cout << nums[i] << " "; 84 cout << endl; 85 return 0; 86 }