归并排序应用1—小和问题 在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。1 2 3 4 5 6 7 [1, 3, 4, 2, 5] 1 左边比 1 小的数,没有; 3 左边比 3 小的数,1; 4 左边比 4 小的数,1、3; 2 左边比 2 小的数,1; 5 左边比 5 小的数,1、3、4、 2; 因此小和为 1 + 1 + 3 + 1 + 1 + 3 + 4 + 2 = 16
算法思想:
将左边小于arr[i]
的和转换成求arr[i]
右边的和,即原问题是求一个数左边比它小,并求和,转换成一个数右边比它大,并求和
merge过程
A.根据递归树,对于1,3,大于1的只有3,产生小和,递归结束,返回上层 [1 1]
B.对于1,3,4,遍历到1有两个比1大;遍历到3有一个比3大 [1 2; 3 1]
C.对于递归右部分2,5,一个比2大 [2 1]
D.返回上层递归,两端数组归并,对于右半部分,比1大的有两个,比3大的有一个,比4大的有一个 [1 4; 2 1; 3 2; 4 1; 5 0]
注意点
一定要排完序才能直到右边多少数比i
大
当左右两侧相同时一定要先归并右侧的
merge
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public int merge (int [] arr, int left, int mid, int right) { int [] temp = new int [right - left + 1 ]; int i = 0 ; int p1 = left; int p2 = mid + 1 ; int res = 0 ; while (p1 <= mid && p2 <= right) { res += arr[p1] < arr[p2] ? (right - p2 + 1 ) * arr[p1] : 0 ; temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 <= mid) { temp[i++] = arr[p1++]; } while (p2 <= right) { temp[i++] = arr[p2++]; } for (int j = 0 ; j < temp.length ; j++) { arr[left + j] = temp[j]; } return res; }
mergeSort
1 2 3 4 5 6 7 8 9 public int mergeSort(int[] arr, int left, int right) { if (left == right) { return 0 ; } int mid = left + ((right - left) >> 1 ); return mergeSort(arr, left, mid) + mergeSort(arr, mid + 1 , right) + merge(arr, left, mid, right); }
smallSum
1 2 3 4 5 6 public int smallSum(int[] arr) { if (arr == null || arr.length < 2 ) { return 0 ; } return mergeSort(arr, 0 , arr.length - 1 ); }
归并排序应用2—逆序对问题
算法思想:
把要统计逆序对个数的序列$C$沿着中间位置分成两个子序列A和B,递归计算子序列$A$和$B$中的逆序对个数,然后合并两个子序列,合并的同时计算所有$(a_i,a_j)$的数对中的逆序对的个数($a_i$在子序列$A$,$a_j$在子序列$B$)
由于子序列$A$和$B$是已经排好序的,在把$A$和$B$合并到$C$时,按照归并排序的合并过程,每次都选两个子序列中最小的元素加入到$C$中
每次$A$中元素$a_i$被加入到$C$中,不会遇到新的逆序,因为$a_i$小于子序列$B$中剩下的每个元素,并且$a_i$出现在$B$中元素的前面。
但每次$B$中的元素$b_j$被加到$C$中,说明它比子序列$A$中剩下的元素都小,由于$B$中所有元素本来都排在$A$后面,所以$b_j$就与$A$中剩下的所有元素都构成逆序对,此时$A$中剩下的元素个数就是与$b_j$构成的逆序对的个数。
图解
伪代码 mergeAndCount(A, B)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 初始化count= 0 ,C = 空 while A和B都不为空 令ai和bj分别为A和B中的首元素 if ai <= bj 把ai加入到输出表C中 A = A - {ai} else 把bj加入到输出表C中 B = B - {bj} count += A中剩下的元素 endIf endWhile if A 为空 把B中剩下元素加入到C else 把A中剩下元素加入到C endIf return 合并结果C和逆序对个数count
sortAndCount (C)
1 2 3 4 5 6 7 8 9 if L只有1 个元素 没有逆序,c1 = c2 = c3 = 0 else 把这个表C均分成两半,A和B (c1, A) = sortAndCount(A) (c2, B) = sortAndCount(B) (c3, C) = mergeAndCount(A, B) endIf return (c1 + c2 + c3, C)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public int merge (int [] arr, int left, int mid, int right) { int [] temp = new int [right - left + 1 ]; int i = left, j = mid + 1 , k = 0 , count = 0 ; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; count += mid - i + 1 ; } } while (i <= mid) { temp[k++] = arr[i++]; } while (j <= right) { temp[k++] = arr[j++]; } for (i = 0 ; i < temp.length; i++) { arr[left + i] = temp[i]; } return count; } public int mergeSort (int [] arr, int left, int right) { if (left == right) { return 0 ; } int mid = left + ((right - left) >> 1 ); return mergeSort(arr, left, mid) + mergeSort(arr, mid + 1 , right) + merge(arr, left, mid, right); } public int reverseSort (int [] arr) { if (arr == null || arr.length < 1 ) { return 0 ; } return mergeSort(arr,0 ,arr.length-1 ); }
堆排序应用 已知一个几乎有序的数组,几乎有序是指,如果把数组排好序的话,每个元素的移动距离可以不超过K,并且K相对于数组来说比较小,请选择一个合适的排序算法针对该数组进行排序
算法思想
遍历前7个数字,放入小根堆,小根堆最小值放0位置上。首先弹出小根堆放入0位置,然后将7位置上的值放入小根堆,弹出小根堆堆顶放入1位置,然后将8位置上的值放入小根堆,…,快结束后将整个小根堆弹出一次放入数组即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void sortedArrDistanceLessK (int [] arr, int k) { PriorityQueue<Integer> heap = new PriorityQueue <>(); int index = 0 ; for (; index < Math.min(arr.length, k); index++) { heap.add(arr[index]); } int i = 0 ; for (; index < arr.length; i++, index++) { heap.add(arr[index]); arr[i] = heap.poll(); } while (!heap.isEmpty()) { arr[i++] = heap.poll(); } }
快速排序应用—荷兰旗问题
经典问题
给定一个数组 arr
,和一个数 num
,请把小于等于 num
的数放在数组的左边,大于 num
的 数放在数组的右边。要求额外空间复杂度 $O(1)$,时间复杂度 $O(N)$。
算法思想
$arr[i]≤num$,arr[i]和小于等于区域下一个数交换,区域右扩,i++
$arr[i]>num$,i++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void partition (int [] arr, int left, int right, int target) { int lessAndEqualBoundary = left - 1 ; int i = 0 ; while (i < arr.length) { if (arr[i] <= target) { swap(arr, ++lessAndEqualBoundary, i++); } else { i++; } } }
荷兰旗问题
给定一个数组 arr
,和一个数 num
,请把小于 num
的数放在数组的左边,等于 num
的数放在数组的中间,大于 num
的数放在数组的右边。要求额外空间复杂度 $O(1)$,时间复杂度 $O(N)$。
算法思想
i
表示小于区域的右边界,k
表示大于区域的左边界
$arr[index]<num$,arr[index]
和小于区域i
下一个数交换,i++
,`index++``
$arr[index]=num$,$index++$
$arr[index]>num$,arr[index]
和大于区域k
前一个数交换,k--
,index
不变(这时候前一个数是刚交换过来的,没有经过检查,index位置不能动)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int [] Netherlands(int [] arr, int left, int right, int target) { int i = left - 1 ; int k = right + 1 ; while (left < k) { if (arr[left] < target) { swap(arr, ++i, left++); } else if (arr[left] > target) { swap(arr, --k, left); } else { left++; } } return new int [] {i + 1 , k - 1 }; }
二分法应用 应用1
在一个有序数组中,找某个数是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public boolean binarySearch (int [] sortedArr, int num) { if (sortedArr == null || sortedArr.length == 0 ) { return false ; } int left = 0 ; int right = sortedArr.length - 1 ; int mid = 0 ; while (left < right) { mid = left + ((right - left) >> 1 ); if (sortedArr[mid] == num) { return true ; } else if (sortedArr[mid] > num) { right = mid - 1 ; } else { left = mid + 1 ; } } return sortedArr[left] == num; }
应用2
在一个有序数组中,找大于等于某个数最左侧的位置 [重要]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int nearestIndex (int [] arr, int value) { int left = 0 ; int right = arr.length - 1 ; int mid = 0 ; int index = -1 ; while (left <= right) { mid = left + ((right - left) >> 1 ); if (arr[mid] >= value) { index = mid; right = mid - 1 ; } else { left = mid + 1 ; } } return index; }
应用3
局部最小值问题
在 情况1 和 情况2 中由于情况1呈下降趋势,情况2呈上升趋势,所以arr数组一定有局部最小值
情况3 mid
比它左边小,比它右边小,所以已经是局部最小,直接返回
情况4 mid
比它左边大,也比它右边大。此时arr[0]
~ arr[1]
是下降趋势, arr[mid-1]
~arr[mid]
是上升趋势,所以在此范围内数组arr
一定存在局部最小。去掉mid
右边的范围,直接找mid
左边。
备注:虽然mid
右边也存在局部最小,但是我们只需要返回一个局部最小,所以我们选择直接往mid
左边找就可以了。
情况5 此时arr[mid]
~ arr[mid+1]
是下降趋势, arr[n-2]
~ arr[n-1]
是上升趋势,所以在此范围内数组arr
一定存在局部最小。去掉mid
左边的范围,找mid
右边。
情况6 此时arr[0]
~ arr[1]
是下降趋势, arr[mid-1]
~ arr[mid]
是上升趋势,所以在此范围内数组arr
一定存在局部最小。去掉mid
右边的范围,找mid
左边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public int LocalMin (int [] arr) { if (arr == null || arr.length == 0 ) { return -1 ; } int N = arr.length; if (N == 1 ) { return 0 ; } if (arr[0 ] < arr[1 ]) { return 0 ; } if (arr[N - 1 ] < arr[N - 2 ]) { return N - 1 ; } int left = 0 ; int right = N - 1 ; while (left < right - 1 ) { int mid = left + ((right - left) >> 1 ); if (arr[mid] < arr[mid - 1 ] && arr[mid] < arr[mid + 1 ]) { return mid; } else { if (arr[mid] > arr[mid - 1 ]) { right = mid - 1 ; } else { left = mid + 1 ; } } } return arr[left] < arr[right] ? left : right; }