归并排序
归并排序应用分治思想,如对一个整数数组升序排序,
分:将数组从中点一分为二,递归下去继续将子数组一分为二,直到拆不了;
治:将拆分成的最小单位逐个处理组合,在这里就是把元素两两排序,逐层往上合并。
//归并排序
public void mergeSort(int[] nums, int l, int r){
int[] tmp = new int[nums.length];
if(l >= r){
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
//开始合并
int i = l, j = mid + 1; //两个指针各指向需要合并的两个数组的首元素
int p = 0; //合并后数组的指针,用于赋值时移动
while(i <= mid && j <= r){
if(nums[i] <= nums[j]){
tmp[p++] = nums[i++];
}else{
tmp[p++] = nums[j++];
}
}
//有一个数组到头了
while(i <= mid){
tmp[p++] = nums[i++];
}
while(j <= r){
tmp[p++] = nums[j++];
}
for(int k = 0; k < r - l + 1; k++){
nums[k + l] = tmp[k];
}
}
再例如对链表的归并排序,如Leetcode 148题
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail){
//空链表,不需要排序
if(head == null){
return head;
}
//分治到最小级别的链表,即长度为1, 直接返回这一个节点
if(head.next == tail){
head.next = null;
return head;
}
//快慢指针,定位中点
ListNode slow = head, fast = head;
while(fast != tail){
slow = slow.next;
fast = fast.next;
if(fast != tail){
fast = fast.next;
}
}
ListNode mid = slow;
//分治
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
//合并
ListNode res = merged(list1, list2);
return res;
}
public ListNode merged(ListNode list1, ListNode list2){
ListNode dummyNode = new ListNode(-1);
ListNode prev = dummyNode, prev1 = list1, prev2 = list2;
while(prev1 != null && prev2 != null){
if(prev1.val <= prev2.val){
prev.next = prev1;
prev1 = prev1.next;
}else{
prev.next = prev2;
prev2 = prev2.next;
}
prev = prev.next;
}
prev.next = prev1 == null ? prev2 : prev1;
return dummyNode.next;
}
}
复杂度分析:
1.归并排序时间复杂度稳定,为O(nlogn)
2.不是原址排序,空间复杂度为O(n)。
快速排序
快排同样利用分治的思想,每次在数组中随机选择一个分区点,然后通过原址交换使分区点左边的元素都小于它,右边的元素都大于它,也就是每次分区都会将分区点的元素排到它的最终位置。每次分区之后再对分区点的左边的数组和右边的数组分别递归分区。
//快排
public int[] quickSort(int[] nums){
quickSelect(nums, 0, nums.length - 1);
return nums;
}
//分治
public void quickSelect(int[] nums, int l, int r){
if(l < r){
int pos = randomizedPartition(nums, l, r);
quickSelect(nums, l, pos - 1);
quickSelect(nums, pos + 1, r);
}
}
//选取区间中一个随机的数作为基准值
public int randomizedPartition(int[] nums, int l, int r){
int i = new Random().nextInt(r - l + 1) + l;
swap(nums,i, r);
return partition(nums, l, r);
}
//原址快排
//取最右边的为基准数,从左向右遍历,a[j] > x则i指针不动,a[j] <= x, 则交换++i指向的元素和
//j遍历到的元素, 这样能保证i指针的左边都是小于基准数的,最后将基准数与i+1位置的数交换
//就实现了让基准数左边都是小于它的,右边都是大于它的,i+1就是基准数排序最终结果该在的位置
public int partition(int[] nums, int l, int r){
int pivot = nums[r];
int i = l - 1;
for(int j = 1; j <= r - 1; ++j){
if(nums[i] <= pivot){
swap(nums, ++i, j);
}
}
swap(nums, i + 1, r);
return i + 1;
}
public void swap(int[]nums, int l, int r){
int tmp = nums[l];
nums[l] = nums[r];
nums[r] = tmp;
}
复杂度分析
1.快速排序是原址排序,空间复杂度为O(1);
2.快速排序的时间复杂度不稳定,最好情况和平均时间复杂度为O(nlogn),最坏情况为O(n^2)。
但如果某些场景只要求找到按某个顺序排列的某个位置上的数,比如算法题中要求求数组中的第k个最大值,或者前k个最大的值,这时用快排是效率较高的,如leetcode215.求数组中第k个最大的元素:
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
//l,r:需要递归的区间左右边界,index:目标下标
public int quickSelect(int[] a, int l, int r, int index){
int q = randomPartition(a, l, r);//计算划分点,包括原址快排
if(q == index){
return a[q];
}else{
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
//选取区间中一个随机的数作为基准值
public int randomPartition(int[] a, int l, int r){
int i = random.nextInt(r - l + 1) + l;//生成[l,r)区间随机位置
swap(a, i, r);
return partition(a, l, r);
}
//原址快排
//取最右边的为基准数,从左向右遍历,a[j] > x则i指针不动,a[j] <= x, 则交换++i指向的元素和
//j遍历到的元素, 这样能保证i指针的左边都是小于基准数的,最后将基准数与i+1位置的数交换
//就实现了让基准数左边都是小于它的,右边都是大于它的,i+1就是基准数排序最终结果该在的位置
public int partition(int[] a, int l, int r){
int x = a[r], i = l - 1; //x为基准数据
for(int j = l; j < r; ++j){
if(a[j] <= x){
swap(a, ++i, j);
}
}
swap(a, i + 1, r);
return i + 1;
}
public void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}