目录
1.归并排序:
Ⅰ.主体思路:
利用递归,先让数组左半边有序,再让数组右半边有序,最后准备一个和原来数组大小相当的辅助数组,以及两个下标,第一个下标指向数组的第一个元素,第二个下标指向数组的中间元素的下一个元素,二者进行比较,哪个小,就先放在辅助数组里面,并且下标自增1,最后哪个下标越界,循环就结束,然后把剩余的元素全部放在辅助数组里面,最后把辅助数组的全部内容copy到原来数组,排序就完成了(这样数组整体就有序了)。
ⅠⅠ.代码部分:
public static void process(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];//辅助数组
int p1 = l, p2 = mid + 1;//两个下标
int i = 0;
while (p1 <= mid && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];//谁小谁就放辅助数组并且自增
}
//事实上下面的两个while只可能运行一个
while (p1 <= mid) {//把剩下的数字放在辅助数组里
help[i++] = arr[p1++];
}
while (p2 <= r) {//把剩下的数字放在辅助数组里
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {//辅助数组内容倒回原数组
arr[l + i] = help[i];
}
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);//数组左半部分有序
mergeSort(arr, mid + 1, r);//数组右半部分有序
process(arr, l, mid, r);//左右有序之后,整体合并有序
}
public static void main(String[] args) {
int[] arr = {2,7,5,8,1,99,58};
mergeSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
运行结果:
ⅠⅠⅠ.一些题目的应用:
小和问题:
在一个数组中,每一个元素左边比当前元素值小的元素值累加起来,叫做这个数组的小和。
例如数组[2,7,3,4,5,1]:
2左边比2小的:没有;
7左边比7小的:2;
3左边比3小的:2;
4左边比4小的:2,3;
5左边比5小的:2,3,4;
1左边比1小的:没有;
所以小和:2+2+2+3+2+3+4 = 18;
显然这个题目我们可以用暴力解法,但是这样不美,体现不出merge的美感。
我们可以这样想 把数组拆分成两半, 用merge的思想:
比如2左边有 4个比2大的 2就算做4次;
7没有;
3 有两个 所以是 2 * 3;
4 有一个 所以是 4 * 1;
5没有;
1没有;
所以这么一来我们只需要计算下标了:因为数组左边有序,右边有序,所以假使右边的元素比左边大,那么右边所有的元素都比左边大,只需要计算右边元素的下标之差就行了。
代码展示:
public static int merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int p1 = l, p2 = mid + 1, res = 0;
int i = 0;
while (p1 <= mid && p2 <= r) {
res += arr[p1] < arr[p2] ? arr[p1] * (r - p2 + 1) : 0;//右边比左边大,右边剩余的都比他大
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}
public static int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return process(arr, l, mid) +
process(arr, mid + 1, r) +
merge(arr, l, mid,r );
/*
返回值是左边的小和 + 右边的小和 + 整体的小和
*/
}
public static int smallSum(int[] arr) {
return process(arr, 0, arr.length - 1);
}
public static void main(String[] args) {
int[] arr = {2,7,3,4,5,1};
int res = smallSum(arr);
System.out.println(res);
}
逆序对问题:(lc :剑指 Offer 51. 数组中的逆序对 - 力扣(LeetCode) (leetcode-cn.com) )
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
这题的思想也是归并了,不多说话,上主要代码:
public int merge(int[] nums, int l, int mid, int r) {
int p1 = l, p2 = mid + 1, res = 0, i = 0;
int[] help = new int[r - l + 1];
while ((p1 <= mid) && (p2 <= r)) {
res += nums[p1] > nums[p2] ? (mid - p1 + 1) : 0;
help[i++] = nums[p1] <= nums[p2] ? nums[p1++] : nums[p2++];
}
while (p1 <= mid) {
help[i++] = nums[p1++];
}
while (p2 <= r) {
help[i++] = nums[p2++];
}
for (i = 0;i < help.length;i++) {
nums[l + i] = help[i];
}
return res;
}
public int process(int[] nums, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >>> 1);
return process(nums, l, mid) +
process(nums, mid + 1, r) +
merge(nums, l , mid, r);
}
public int reversePairs(int[] nums) {
if (nums.length < 2) {
return 0;
}
int ret = process(nums, 0, nums.length - 1);
return ret;
}
2.快速排序:
Ⅰ.荷兰国旗问题引入:
简单版 :给定一个数组和某个数,要求你把小于或者等于这个数的数字放在数组的左边,大于给定数字的内容放在数组右边。
思路:
我们创造一个小于等于给定数字的边界和一个下标,进行以下操作:
在下标往数组右边运动的时候,如果碰到了一个比给定数字小的数字,把这个数字和我们准备好的边界的下一个数字进行交换,边界扩张,下标往后走1个,如果遇到了一个更大的数字,边界不动,下标继续往右走,当我们的i走遍历完数组之后,循环推出。
代码部分:
public static void swap(int[] arr, int p, int q) {
int t = arr[p];
arr[p] = arr[q];
arr[q] = t;
}
public static void partition(int[] arr, int k) {
int less = -1,i = 0;//less是边界
while (i < arr.length) {
if (arr[i] <= k) {
swap(arr, ++less, i++);//比给定数字小,与边界的下一个数字交换,边界扩张
} else {//比给定数字大,i++;
i++;
}
}
}
public static void main(String[] args) {
int[] arr = {1,2,4,3,2,7};
partition(arr, 3);
System.out.println(Arrays.toString(arr));
}
升级版:
在原有的基础上,我们增加一条规则:等于给定数字的放在数组中间,其余规则照常。
思路:
在原有的思路上我们加工一下:我们再给定一个大于边界放在数组的右边,如果碰到一个大于的数,就把他与大于边界的下一个数字进行交换,大于边界扩张,此时注意,i不动!!!,此时退出循环的条件是i与大边界碰面。
代码:
public static void swap(int[] arr, int p, int q) {
int t = arr[p];
arr[p] = arr[q];
arr[q] = t;
}
public static void partition(int[] arr, int k) {
int less = -1,i = 0, more = arr.length;//less是小于边界 more是大于边界
while (i < more) {
if (arr[i] < k) {
swap(arr, ++less, i++);//比给定数字小,与边界的下一个数字交换,边界扩张
} else if (arr[i] > k){ //比给定数字大,与大于边界的下一个数字交换,大于边界扩张
swap(arr, --more, i);
} else {
i++;
}
}
}
public static void main(String[] args) {
int[] arr = {1,2,4,3,2,7,3,4,1};
partition(arr, 3);
System.out.println(Arrays.toString(arr));
}
有了以上的partition的过程,我们就可以引入快排了。
ⅠⅠ.快排的引入:
可以说快排就是分治思想的引入,我们可以仿照这个partition的过程,把数组用partition分出两个边界,并且再这两个边界的基础上递归下去,这样当我们走到递归的出口的时候,排序也就完成了。
这里还有一点点修正的地方,我们要用random方法,等概率的在数组中取一个数,与数组最后一个元素交换,把这个交换的元素当作基准做partition,由于random方法是等概率再【0,1)中取任何小数,所以我们的快排的数学概率求期望的时间复杂度也会达到快排所要求的O(nlogn)的时间复杂度。
代码:
public static void swap(int[] arr, int p, int q) {
int t = arr[p];
arr[p] = arr[q];
arr[q] = t;
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
swap(arr, l + (int) ((r - l + 1) * Math.random()), r);//取中间随机值
int[] p = partition(arr, l, r);//分治取边界
//边界取递归
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1, more = r;//由于前面已经交换,所以此时r即使给定的partition数字
//partition过程
while (l < more) {
if (arr[l] < arr[r]) {
swap(arr,++less, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);//partition完成后交换
return new int[]{less + 1, more};//返回所求边界
}
public static void main(String[] args) {
int[] arr = {2,6,1,4,3,7,10,8};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
3.归并排序:
Ⅰ大根堆的介绍:
为了说明归并排序,我们可以把数组从第一个元素出发的数字当作二叉树的根节点,然后依次把他展开为二叉树,物理上是数组存储,逻辑上是二叉树的结构。
有了二叉树,那么大根堆就是每一个根节点都是子树的最大值。
这样就是一个大根堆的结构
ⅠⅠheapInsert过程:
比如我们给定的数组不是一个大根堆,我们就要对他进行调整,heapInsert的过程是一个上浮的过程,比如某个子节点的值比父节点的值大,我们就可以通过heapInsert把他调整为一个大根堆。
那么问题是,怎么找到自己的父? 答案是假设当前子节点的下标是i,那么父节点的下标就是
(i - 1 )/2
代码:
public static void swap(int[] arr, int p, int q) {
int t = arr[p];
arr[p] = arr[q];
arr[q] = t;
}
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 main(String[] args) {
int[] arr = {2,1,4,3,5,7,6,8};
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
System.out.println(Arrays.toString(arr));
}
这显然就是我们要求的大根堆。
ⅠⅠⅠheapify过程
根据上面的heapInsert过程,我们可以联想到heapify过程显然就是父节点与子节点的交换过程了,如果一个父节点比子节点小,那么我们就要进行一个heapify过程。
在上面给出的例子中,如果我们把数组第一个数字拿走,并且把他换成1,显然他就不是一个大根堆,我们就要进行heapify
代码:
public static void swap(int[] arr, int p, int q) {
int t = arr[p];
arr[p] = arr[q];
arr[q] = t;
}
public static void heapify(int[] arr, int index, int heapSize) {
int left = 2 * index + 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 = 2 * left + 1;
}
}
public static void main(String[] args) {
int[] arr = {2,1,4,3,5,7,6,8};
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
System.out.println(Arrays.toString(arr));//数组调整为大根堆
arr[0] = 1;//修改根节点的值
int heapSize = arr.length;//堆的大小
heapify(arr, 0, heapSize);//heapify过程
System.out.println(Arrays.toString(arr));
}
以上代码的一点文字解释:
largest变量:用来记录左右孩子的最大值,最后与index,也就是父节点比较,如果没有父节点大,那么这时候已经是一个大根堆了,无需调整,如果比父节点大,那么就进行调整
index: 进行heapify的数字的下标
ⅠⅤ.堆排序:
通过上述的heapInsert过程和heapify过程,我们就可以进入堆排序的过程了。
我们先对整个数组进行一个heapInsert过程,然后把第一个元素和最后一个交换,同时heapSize--,然后再对第一个元素进行heapify,等到heapsize编程0的时候,排序就完成了
代码:
public static void heapSort(int[] arr) {
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);//heapify过程
swap(arr, 0, --heapSize);
}
}
public static void main(String[] args) {
int[] arr = {2,8,1,3,2,10,18,13,5};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
Ⅴ.一个小小的问题:
给定一个几乎有序的数组和一个整数k,要求再数组排完序后,排序后的数字的位置与原来的位置相差的位置不超过k
思路:准备一个小根堆,把前 k 个元素放入小根堆中,然后拿出一个,放在数组的第一个位置,然后再往后加入数组的一个元素,这样就可以达到我们要求的效果了
ps:再java里面可以用PriorityQueue来进行这个容器来操作,他的底层实现就是小根堆
代码:
public static void main(String[] args) {
int[] arr = {1,2,4,5,3,4,5,8,6,7,8,10};
PriorityQueue<Integer> heap = new PriorityQueue<>();
int k = 2;//假设k是2
int l = 0, i = 0;
for ( i = 0; i < k; i++) {
heap.add(arr[i]);//前k个元素放入heap中
}
while (i < arr.length) {
arr[l++] = heap.poll();//弹出一个加一个
heap.add(arr[i++]);
}
while (!heap.isEmpty()) {
arr[l++] = heap.poll();//最后全部弹出
}
System.out.println(Arrays.toString(arr));
}
4.桶排序:
(声明:以下的桶排序的数字都是十进制的)
Ⅰ.桶思想:
对于数组里面的每个数字,我们先获得里面最大数字多少位,然后准备十个桶(0-9号),进行以下操作:
根据个位数的大小(个位数是几就放在几号桶),把他依次装在桶里面,然后倒出。
再根据十位数的大小(同上),再装到桶里面,倒出。。
以此类推,当达到最大位数的时候,循环推出,排序就排序好了。
上述思想太简单了,不详细写。。
(注意桶要先进先出,从左到右依次放入哦)
ⅠⅠ.升级版:
public static int maxDigit(int[] arr) {//获取数组中最大位数
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max,arr[i]);
}
int res = 0;
while (max != 0) {
max /= 10;
res++;
}
return res;
}
public static int getDigit(int num, int d) {//获取数组中元素的某个位置的数字
return num / (int)(Math.pow(10, d)) % 10;
}
public static void radixSort(int[] arr, int l, int r, int digit) {
final int radix = 10;//基数
int[] bucket = new int[r - l + 1];//桶
int i, j;
for (int d = 1; d <= digit; d++) {//因为最大是digit位数,所以循环要进行digit次
int[] count = new int[radix];//字典计数
for (i = l; i <= r; i++) {
j = getDigit(arr[i],d - 1);//把每一位放在count里面
count[j]++;
}
for (i = 1; i < radix ;i++) {
count[i] += count[i - 1];//前缀和处理优化
}
for (i = r; i >= l; i--) {
j = getDigit(arr[i],d - 1);
bucket[count[j] - 1] = arr[i];//根据自己的位数的大小,逆序倒在桶里面
count[j]--;
}
for (i = l, j = 0; i <= r; i++, j++) {//桶里面依次倒出
arr[i] = bucket[j];
}
}
}
public static void main(String[] args) {
int[] arr = {101,25,97,33,88,56,123,491,4,1};
radixSort(arr, 0, arr.length - 1, maxDigit(arr));
System.out.println(Arrays.toString(arr));
}
ⅠⅠⅠ解释:
radixSort
里面的一些小细节:由于我们做了前缀和优化,就可以由每个数字的字典序 - 1找到自己应该在的位置哦,不理解可以多画图
以上就是一些排序的小总结~~