一个排序的重要性不仅仅局限于他的功能,思路的重要性
重要的排序算法:归并排序、快排、堆排
- 归并排序的思想:分治思想和master时间复杂度公式,合的思想
- 快排的思路:分的思想
- 堆排的思想:堆结构的思想
归并的应用-小和
public static 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 static int merge(int[] arr, int left, int mid, int right) {
int[] help = 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;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[left++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[left+j] = help[j];
}
return res;
}
解析计算和步骤
- 134和25合并的时候找和
- 1比2小,那么就有2个1
- 3比2大,比5小,那么就只有1个3
- 4比2大,比5小,那么就只有1个4
- 2 * 1+1 * 3+1 * 4=9
逆序对变形
class Solution {
// 归并排序的应用-分治思想
public int reversePairs(int[] nums) {
if (nums == null || nums.length <2) return 0;
return mergeSort(nums, 0, nums.length-1);
}
public int mergeSort(int[] nums, int left, int right) {
if (left == right) return 0;
int mid = left + ((right-left)>>1);
return mergeSort(nums, left, mid)
+ mergeSort(nums, mid+1, right)
+ merge(nums, left, mid, right);
}
public int merge(int[] nums, int left, int mid, int right) {
int[] help = new int[right-left+1];
int i = 0;
int p1 = left;
int p2 = mid+1;
int res = 0;
while (p1 <= mid && p2 <= right) {
// 主要是这里的区别:如果是求所有的和则要写出(right-p2+1)*nums[p1]
res += nums[p1] > nums[p2] ? (right-p2+1) * 1 : 0;
help[i++] = nums[p1] > nums[p2] ? nums[p1++] : nums[p2++];
}
while (p1 <= mid) {
help[i++] = nums[p1++];
}
while (p2 <= right) {
help[i++] = nums[p2++];
}
for (int k = 0; k < help.length; k++) {
nums[left+k] = help[k];
}
return res;
}
}
排序链表
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗
// 归并排序
public ListNode sortList(ListNode head) {
return head == null ? null : mergeSort(head);
}
private ListNode mergeSort(ListNode head) {
if (head.next == null) {
return head;
}
// 利用快慢指针将链表分成两段
ListNode p = head, q = head, pre = null;
while (q != null && q.next != null) {
pre = p;
p = p.next;
q = q.next.next;
}
pre.next = null;
ListNode l = mergeSort(head);
ListNode r = mergeSort(p);
return merge(l, r);
}
ListNode merge(ListNode l, ListNode r) {
ListNode dummyHead = new ListNode(0);
ListNode cur = dummyHead;
while (l != null && r != null) {
if (l.val <= r.val) {
cur.next = l;
cur = cur.next;
l = l.next;
} else {
cur.next = r;
cur = cur.next;
r = r.next;
}
}
if (l != null) {
cur.next = l;
}
if (r != null) {
cur.next = r;
}
return dummyHead.next;
}
快排
快排的引例-荷兰国旗问题
- 思想
- 代码
public static int[] partition(int[] nums, int left, int right, int targrt) {
int less = left - 1;
int more = right + 1;
int index = left;
while (index < more) {
if (nums[index] < targrt) {
// 交换过来的数已经比较过了,直接可以++
swap(nums, ++less, index++);
} else if (nums[index] > targrt) {
// 这里cur不加,是因为交换过来这个数,还没有比较多,需要再比较一次
swap(nums, --more,index);
} else {
index++;
}
}
// 返回左右边界
return new int[] {less+1, more-1};
}
public static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
经典快排
public static void quickSort(int[] nums, int left, int right) {
if (left < right) {
// 荷兰国旗划分好之后,继续递归划分剩下两个区域
int[] p = partition(nums, left, right);
quickSort(nums, left, p[0]-1);
quickSort(nums, p[1]+1, right);
}
}
// 默认以最后一个数做划分
public static int[] partition(int[] nums, int left, int right) {
int less = left - 1;
int more = right+1;
int target = nums[right]
while (left < more) {
if (nums[left] < target) {
// 交换过来的数已经比较过了,直接可以++
swap(nums, ++less, left++);
} else if (nums[left] > target) {
// 这里cur不加,是因为交换过来这个数,还没有比较多,需要再比较一次
swap(nums, --more,left);
} else {
left++;
}
}
// 因为最后一个位置没有参与遍历,这里是归位(边界处理的炫技),加target就可以去掉这个步骤
// swap(nums, more, right);
// 返回等于区域的范围
return new int[] {less+1, more-1};
}
随机快排
- 经典快排缺点:和数据有关系,经典快排总拿最后一个数(某个固定的数),会出现退化的情况
- 例如:1234567这个的时间复杂度就会退化成(O(N^2))
- 随机快排的时间复杂度是期望时间复杂度(O(N*logN))
- 随机快排是最常用,最重要的排序,工程上的排序都是用随机快排实现的,工程上不允许出现递归,所以工程上的快排是迭代版的快排(之后需要掌握快排怎么转换非递归版本)
public static void quickSort(int[] nums, int left, int right) {
if (left < right) {
// 随机选一个数和最后一个数交换
swap(nums,left+(int)(Math.random()*(right-left+1)),right);
// 荷兰国旗划分好之后,继续递归划分剩下两个区域
int[] p = partition(nums, left, right);
quickSort(nums, left, p[0]-1);
quickSort(nums, p[1]+1, right);
}
}
应用:绕过原始数据状况
- 随机函数
- hash函数
堆排
- 堆结构非常重要
- 堆其实是完全二叉树
在完全二叉树的基础上,对其做一些处理就可以得到大、小根堆结构 - 大根堆:任何一颗子树的最大值都是头部
- 小根堆:任何一颗子树的最小值都是头部
- 对完全二叉树做处理
完全二叉树可以基于数组实现(在逻辑上实现的,实际结构还是数组)
左孩子:2*i + 1
右孩子:2*i + 2
父节点:(i - 1)/2(重要,基于这个实现完全二叉树到堆结构改造)
- 实现大根堆改造
- 2-1-3中不是大根堆,让3根据(i-1/2)计算得到父节点进行交换,此时得到0-2上的部分大根堆
- 同理6插入进来,进行交换,得到0-3上的大根堆
- 同理使得整个数组编程大根堆
// 交换
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index-1)/2]) {
swap(arr, index, (index-1)/2);
// 交换之后让指针变为父节点,再次和父节点比较(直到每一层的父节点都是最大的)
index = (index-1)/2;
}
}
// 得到0-i之间的大根堆
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
- 建立大根堆的时间复杂度
需要比较的个数就是二叉树的高度(完全二叉树的高度是logN),所以的加起来(log1+log2+log3+…logn-1=O(n)),所以建立大根堆的时间复杂度o(n) - 大根堆的另一个操作(heapify):用于解决大根堆中的某一个数发生变化,使得大根堆变成非大根堆,进行调整变回大根堆的操作
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
// 求左右孩子最大值
int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left+1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
// 还是得到交换这个的位置的左孩子
index = largest;
left = index * 2 + 1;
}
}
堆结构的重要性:
- 堆结构的重要性:贪心问题的结构几乎都是堆结构搞定的,堆又被称为优先级队列
- 堆结构的调整复杂度只和层数有关系,也就是调整一次(加一个数或者减一个数)的时间复杂度是Log(n)
- 来感受一下log的时间复杂度的魅力:log(400,0000,0000)=10.6(400亿的时间复杂度大概是10)
堆排序 - 理解了堆结构和堆的几个操作,堆排序就比较简单
- 首先是将大根堆和最后一个数交换,此时最大的数在最后(这个数已排好序)
- 利用headify(下沉操作),使得0-n-2的数重新变成大根堆
- 重复步骤2操作,将大根堆和倒数第二个数交换(那个后两个数也将拍好序)
- 重复步骤
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
// 建立大根堆
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
// 建立大根堆
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index-1)/2]) {
swap(arr, index, (index-1)/2);
index = (index-1)/2;
}
}
// 重建大根堆操作
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left+1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
堆排的应用
如果需要随时接收数据,并且需要随时读取所有数据的中位数,则利用堆进行运算
算法流程:
1.首先创建两个堆,一个大根堆,一个小根堆
2.将第一个数据防入大根堆,第二个数据到来之后跟大根堆中的数据进行比较,如果数据小于大根堆中的根,则放入大根堆中,此时大根堆中会有两个数据,而小根堆中没有数据。
3.如果出现大根堆和小根堆中的数目不相等,相差2,则将数据较多的堆的父节点弹出,放入数据较少的数据堆中当父节点。堆顶需要删掉则可以执行将堆底最后的数据跟堆顶换位,将堆的数据heapsize减少,之后再进行heapify,则重新调整为大根堆。
经过上述三个步骤,则可以实现大根堆中的数据小于小根堆中的数据,中位数的数据只会从大根堆和小根堆的父节点中选出,随时进入数据,则可以随时调整,复杂度较低。