002 认识O(nlogn)的排序

master公式

\(T(N) = a*T(N/b) + O(n^d)\)

  • \(log(b,a) > d\),复杂度为\(O(N^{log_b^a})\)
  • \(log(b,a) = d\),复杂度为\(O(N^d*logN)\)
  • \(log(b,a) < d\),复杂度为\(O(N^d)\)

二分法-递归

public class Code01_GetMax {
	
	public int getMax(int[] arr) {
		return process(arr, 0, arr.length - 1);
	}
	
	public int process(int[] arr, int L, int R) {
		if (L == R) {
			return arr[L];
		}
		int mid = L + ((R - L) >> 1);//中点
		int leftMax = process(arr, L, mid);//找左边的最大值
		int rightMax = process(arr, mid + 1, R);//找右边的最大值
		return Math.max(leftMax, rightMax);
	}
}

时间复杂度:\(O(N^{log_b^a})\)

归并排序

左半部分排序

右半部分排序

两部分都有序了之后

归并

继续递归

public class Code02_MergeSort {
	
	public void process(int[] arr, int L, int R) {
		if (L == R) {
			return;
		}
		int mid = (L + ((R - L) >> 1));
		process(arr, L, mid);//对左半部分排序 左边有序
		process(arr, mid + 1, R);//对右半部分排序  右边有序
		merge(arr, L, mid, R);//归并两个部分
	}
	
	public void merge(int[] arr, int L, int M, int R) {
		int[] help = new int[R - L + 1];//辅助数组
		int i = 0;
		int p1 = L;
		int p2 = M + 1;
		while (p1 <= M && p2 <= R) {
			//把小的数放前面
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
		}
		//如果前半部分的数更多,没有比较完,把剩余的数放在help数组中
		while (p1 <= M) {
			help[i++] = arr[p1++];
		}
		//如果后半部分额数更多,没有比较完,把剩余的数放在help数组中
		while (p2 <= R) {
			help[i++] = arr[p2++];
		}
		//把help数组中的数放入到原数组中
		for (i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
	}
}

时间复杂度:\(O(nlogn)\)

归并排序的扩展

小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。

例如数组1、3、4、2、5的小和。

左边比1小的数:无

左边比3小的数:1

左边比4小的数:1、3

左边比2小的数:1

左边比5小的数:1、3、4、2

可以做如下转换:

1的右边有4个大于1的数,1在小和中参与了4次

3的右边有2个大于3的数,3在小和中参与了2次

4的右边有1个大于4的数,4在小和中参与了1次

2的右边有1个大于2的数,2在小和中参与了1次

5的右边没有大于5的数,

需要进行归并的左右两部分已经各自有序。

判断左部分的数是否会对右部分的数产生小和。

归并的新数组的小和 = 左部分的答案 + 右部分的答案 + 跨越左右产生的答案。左右部分的小和在归并时已经计算出来了,要计算当前归并时产生的小和。

每个数字都不会重复计算或者漏掉,例如1,在1、3归并时,可以得出1在1、3数组中的小和;然后和4归并,得出1在1、3、4数组中的小和,它不会再去计算1、3中出现的小和,归并过程中,只会计算1、3数组和4数组,两个数组归并时产生的小和。经过两次归并后,就可以得出1在1、3、4这个数组中一共出现了多少次小和。同理,继续归并。可以计算1在更大的数组中的小和,数组范围是依次扩大的,所以不会漏掉。而归并的时候,只在归并的时候会产生小和,要归并的两个小数组的小和已经计算过了,他们已经有序,不用再计算他们的小和,所以不会重复。

当右边的数y大于左边的数x时,那么y右边的所有数也大于x,所以x会参与进这些数的小和计算中,所以x被计算了(r-p2+1)次。

所以当左右部分指到的数都相等时,先拷贝右边的数,也就是右边的指针移动,当右边出现一个数大于左边指针指向的数x时,可以知道有r - p2 + 1个数是大于x的,所以x在小和中出现了r - p2 + 1次。

public class Code03_SmallSum {
	
	public int smallSum(int[] arr) {
		if (arr == null || arr.length < 2) {
			return 0;
		}
		return process(arr, 0, arr.length - 1);
	}
	
	public int process(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		int mid = l + ((r - l) >> 1);
		return process(arr, l, mid) //左侧l~mid的小和
				+ process(arr, mid + 1, r) //右侧mid+1~r的小和
				+ merge(arr, l, mid, r); //mid+1~r中 比l~mid+1中大的小和
	}
	
	public int merge(int[] arr, int l, int m, int r) {
		int[] help = new int[r - l + 1];
		int i = 0;
		int p1 = l;
		int p2 = m + 1;
		int res = 0;
		
		while (p1 <= m && p2 <= r) {
			//只有arr[p1] < arr[p2]时,才会产生小和,并且可以直接算出右边部分一直到arr[p2]处有多少个数大于arr[p1]。等于时指针继续移动,和普通归并排序不同的点
			res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;//r - p2 + 1:当前右部分指向的数,有这么多个大于左部分指向的数
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
		}
		
		while (p1 <= m) {
			help[i++] = arr[p1++];
		}
		while (p2 <= r) {
			help[i++] = arr[p2++];
		}
		
		return res;
	}
}

逆序对问题

在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对。

小和问题的逆序版,从大到小归并排序,找出左边比右边大的数

public class Code04_ReversePairs {
	public int reversePairs(int[] record) {
		if (record == null || record.length < 2) {
			return 0;
		}
		
		return process(record, 0, record.length - 1);
    }
	
	public int process(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		int mid = (l + r) / 2;
		return process(arr, l, mid) + 
				process(arr, mid + 1, r) + 
				merge(arr, l, mid, r);
	}
	
	public int merge(int[] arr, int l, int m, int r) {
		int[] help = new int[r - l + 1];
		int i = 0;
		int p1 = l;
		int p2 = m + 1;
		int cnt = 0;
		
		while (p1 <= m && p2 <= r) {
			cnt += arr[p1] > arr[p2] ? (r - p2 + 1) : 0;
			help[i++] = arr[p1] > arr[p2] ? arr[p1++] : arr[p2++];
		}
		
		while (p1 <= m) {
			help[i++] = arr[p1++];
		}
		
		while (p2 <= r) {
			help[i++] = arr[p2++];
		}
		
		for (int j = 0; j < help.length; j++) {
			arr[l + j] = help[j];
		}
		
		return cnt;
	}
}

快速排序

假设划分值的选取是每次选当前数组的最后一个元素

划分值将数组分成左右两半,然后又分别对左右数组,选取划分值,进行划分。

重复

荷兰国旗问题

问题一:快排1.0

给定一个数组arr和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)

假设有一个区域里的数全是小于等于num的,遍历数组:

  • 小于等于num,就和区域里的下一个数交换,然后扩大区域,也就是把这个小于等于num的数放进区域里。

  • 大于num,指向下一个数

  • 以当前数组的最后一个数num作为划分,小于等于num的数放在小于区,这一轮放完之后,把大于区的第一个数,也就是第一个大于num的数字,和划分值交换,划分值就放到了前面,因为num也是小于等于num的数。

    继续递归

问题二:快排2.0

给定一个数组arr和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)

假设有一个区域放小于num的数,有一个区域放大于num的数,遍历数组

  • 小于num,和小于区的下一个数交换,扩大小于区
  • 等于num,i++
  • 大于num,和大于区的前一个数交换,大于区左扩。但是要注意此时i不变,因为交换了之后,交换过来的值位于i处,大小未知,还要和num做比较
  • 每一轮交换完了之后,把划分值和大于区的第一个数交换,让它交换到等于区

每一次递归后,被选作划分的num总是在中间,可能不止一个等于num的数,它们的位置就被排好了,以后都不用动了。


快排1.0和2.0的最差情况都是\(O(n^2)\):由于每一次划分的时候都选的是当前要划分数组的最后一个元素,如果数组已经有序,每次递归/划分都要和当前数组的前面每一个数比较,每次递归结束就只排好一个数,一共有n个数。所以是\(O(n^2)\)

快排3.0:随机快速排序

如果每次的划分值几乎位于中间,那么左右两边的递归规模就差不多相等。根据master公式:,可以达到一个比较好的时间复杂度。

随机选择划分值,即使是数组已经有序,划分值也是随机的,不会出现上面的最差情况。

划分值的选取可能在1/3处,1/2处,1/4处等等,每一处出现的可能性是1/n,求期望,可以得到O(nlogn)的时间复杂度。

public class Code05_QuickSort {
	
	public void sort(int[] arr) {
		
	}

	
	public void quickSort(int[] arr, int L, int R) {
		if (L < R) {
			int random = L + (int) (Math.random() * (R - L + 1));//在[L, R]之间随机选择一个数
			/**
			 * 将随机选择的数与数组最后一个值交换
			 */
			swap(arr, random, R);
			int[] p = partition(arr, L, R);//返回长度为2的数组,小于区的左边界和右边界
			quickSort(arr, L, p[0] - 1);//小于区
			quickSort(arr, p[1] + 1, R);//大于区
		}
	}
	
	/**
	 * 默认以arr[r]做划分
	 * 返回数组长度为2,即等于区域的左边界和右边界的数组
	 */
	public int[] partition(int[] arr, int L, int R) {
		int less = L - 1;//小于区右边界
		int more = R;//大于区左边界,以arr[r]做划分所以左边界是从R开始而不是R+1
		while (L < more) {//遍历的数字遇到大于区就结束
			/**
			 * 如果遍历到的数小于划分值,就放在小于区的下一个位置,并且扩大小于区
			 */
			if (arr[L] < arr[R]) {
				swap(arr, ++less, L++);//++less:小于区的下一个位置,同时扩大小于区; L++:遍历下一个数
			}else if (arr[L] > arr[R]) {
				/**
				 * 如果遍历到的数大于划分值,就放在大于区的前一个位置,并左扩大于区
				 */
				swap(arr, --more, L);//--more:大于区的前一个位置; L:L不能++,交换后还要再对这个数进行比较
			}else {
				/**
				 * 如果遍历到的数等于划分值,则继续遍历下一个值
				 */
				L++;
			}
		}
		/**
		 * 把划分值放在大于区的第一个
		 * 划分结束了,划分值还在数组的最后一个,划分值应该位于等于区
		 * 此时more就指向等于区的右边界
		 */
		swap(arr, more, R);
		
		/**
		 * less是小于区的右边界,less+1就是等于区的左边界
		 * more指向等于区的右边界
		 */
		return new int[] {less + 1, more};
	}
	
	public void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}
}
posted @ 2023-11-20 11:01  平平无奇的five  阅读(25)  评论(0)    收藏  举报