2.1 归并排序
![](https://i-blog.csdnimg.cn/blog_migrate/bd11a800cfcd447991651e77f10f87c6.png)
- 时间复杂度为
,额外的空间复杂度为
- 归并排序整体就是一个简单递归, 左边排好序、 右边排好序,再让其整体有序。让其整体有序的过程里用了排外序方法
![](https://i-blog.csdnimg.cn/blog_migrate/f82df749354d69e2a84be625e50f91e0.jpeg)
// 归并, O(Nlog(N))
public void MergeSort (int[] arr, int left, int right) {
if (left == right) {
return;
}
int mid = left + ((right - left) >> 1);
MergeSort(arr, left, mid); // 拆分左半部分
MergeSort(arr, mid+1, right); // 拆分右半部分
Merge(arr, left, right, mid); // 每次拆分完之后就合并, 合并的都是有序的数组
}
public void Merge (int[] arr, int left, int right, int mid) {
int[] help = new int[right - left + 1];
int i = 0;
int p1 = left;
int p2 = mid + 1;
while (p1 <= mid && p2 <= right) {
if (arr[p1] < arr[p2]) {
help[i++] = arr[p1++];
} else {
help[i++] = arr[p2++];
}
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[left+i] = help[i];
}
}
2.2 归并排序的扩展
2.2.1 小和问题
在一个数组中, 每一个数左边比当前数小的数累加起来, 叫做这个数组的小和。 求一个数组的小和。
例子: [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
public int SmallSum (int[] arr, int left, int right) {
if (left == right) {
return 0;
}
int mid = left + ((right - left) >> 2);
SmallSum2(arr, left, mid);
SmallSum2(arr, mid + 1, right);
return SmallSum2Merge(arr, left, right, mid);
}
static int count = 0;
public int SmallSumMerge (int[] arr, int left, int right, int mid) {
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) {
if (arr[p1] < arr[p2]) {
count += (right - p2 + 1) * arr[p1]; // 计算右组有多少个数小于左组arr[p1]这个数
help[i++] = arr[p1++];
} else {
help[i++] = arr[p2++];
}
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[left + i] = help[i];
}
return count;
}
2.2.2 逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请返回逆序对的数量。
public int InverseOrder2 (int[] arr, int left, int right) {
if (left == right) {
return 0;
}
int mid = left + ((right - left) >> 1);
InverseOrder2(arr, left, mid);
InverseOrder2(arr, mid + 1, right);
return InverseOrder2Merge(arr, left, right, mid);
}
static int count = 0;
public int InverseOrder2Merge (int[] arr, int left, int right, int mid) {
int i = 0;
int p1 = left;
int p2 = mid + 1;
int[] help = new int[right - left + 1];
while (p1 <= mid && p2 <= right) {
if (arr[p1] > arr[p2]) { // 逆序排序
count += (right - p2 +1); // 不理解, 计算右组中有多少个数小于arr[p1]
help[i++] = arr[p1++];
} else {
help[i++] = arr[p2++];
}
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[left+i] = help[i];
}
return count;
}
2.3 堆结构与堆排序
![](https://i-blog.csdnimg.cn/blog_migrate/5372d31de331a4aebdd2ca705eda7d67.png)
![](https://i-blog.csdnimg.cn/blog_migrate/853a7d22e552b4698b52fd823f8a5087.jpeg)
![十大经典排序算法动画演示](https://i-blog.csdnimg.cn/blog_migrate/a5ed4fa8b37bb7759526be8b3a34208d.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/c741d1aef760cdabd1ff672b95662cea.gif)
堆在程序中可用数组实现,但逻辑上就是一个完全二叉树, 完全二叉树是指最后一层从左向右增加,其他层都是满层的二叉树。 假设二叉数起始位置对应数组下标 0 ,对于 i 节点,它的左孩子是 2i+1,它的右孩子是 2i+2,父节点是 (i-1)/2。 堆可分为大根堆与小根堆,完全二叉树中如果每棵子树的最大值都在顶部就是大根堆,如果每棵子树的最小值都在顶部就是小根堆。 如何实现大根堆,用户希望调用 void add (num) 方法得到符合大根堆要求的数组,可以这样考虑:每加入一个数,都和自己的父结点的数做比较,如果比自己的父亲结点的位置大,就交换位置,否则不做交换。 这种从下至上的过程如 HeapInsert 所示:
public void HeapInsert (int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) { // (index - 1) / 2) 是 index 的父节点
Swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
int popmax(),用户希望调用这个函数返回堆结构的最大值,并删除掉这个值,剩下的值保持大根堆的结构。 那么可以取出 arr[0] 的值,将 arr 中最后一个值放到 arr[0] 位置上,它有2个子节点,2个孩子作比较,选出大的和它进行比较,若孩子大,则交换位置。 来到新的位置后继续向下进行比较,直到自己的子节点比自己小 or 没有左孩子节点为止。 这种从上到下的过程称为Heapify
// 某个数在index位置,能否往下移动,int popmax()
public void Heapify (int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int largest = 0;
while (left < heapSize) {
// 两个孩子中,谁的值大,把下标给largest
if (right < heapSize && arr[left] < arr[right]) {
largest = right;
} else {
largest = left;
}
// 父和较大的孩子之间,谁的值大,把下标给largest
if (arr[largest] < arr[index]) {
largest = index;
}
if (largest == index) {
break;
}
Swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
right = index * 2 + 2;
}
}
堆排序,时间复杂度为
// 堆排序, O(Nlog(N))
public void HeapSort (int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 把arr转换成大根堆结构, O(Nlog(N))
for (int i = 0; i < arr.length; i++) { // O(N)
HeapInsert(arr, i); // O(log(N))
}
// 对大根堆进行排序,O(Nlog(N))
int heapSize = arr.length;
Swap(arr, 0, --heapSize);
while (heapSize > 0) { // O(N)
Heapify(arr, 0, heapSize); // O(logN)
Swap(arr, 0, --heapSize); // O(1)
}
}
2.4 堆排序扩展题目——小范围排序
已知一个几乎有序的数组, 几乎有序是指, 如果把数组排好顺序的话, 每个元素移动的距离可以不超过k, 并且k相对于数组来说比较小。 请选择一个合适的排序算法针对这个数据进行排序。
先跳过,留个坑,回头补。
- in-place (原地算法):占用常数内存,不占用额外内存,靠输出覆盖输入。
- out-place:占用额外内存。
2.5 荷兰国旗问题
问题一 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。 数组左边和右边都可以是无序的。 要求额外空间复杂度O(1),时间复杂度O(N)。
![](https://i-blog.csdnimg.cn/blog_migrate/cc73df1a1e26b6f592cb4baf63731b2e.jpeg)
定义指针p1,arr[p1] 及 arr[p1] 左边的数都比 num 小,即 p1 是小于区的右边界。 遍历数组 arr,当 arr[i] <= num 时,交换 arr[p1] 与 arr[i] ,p1++。
public static void NetherlandsFlag (int[] arr, int num) {
int p1 = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] <= num) {
Swap(arr, p1, i);
p1++;
}
}
}
public static void Swap (int[] arr, int i, int j) {
if (i != j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
问题二(荷兰国旗问题)给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。 要求额外空间复杂度O(1),时间复杂度O(N)。
定义双指针p1,p2。 arr[p1] 及 arr[p1] 左边的数都比 num 小,arr[p2] 及 arr[p2] 左边的数 (不包括arr[p1] 及 arr[p1] 左边的数) 等于num。 p1 是小于区的右边界,p2 是等于区的右边界。 遍历数组 arr,当 arr[i] < num 时,交换 arr[p1] 与 arr[i] ,p1++,p2++;当 arr[i] == num 时,交换 arr[p2] 与 arr[i] ,p2++。
![](https://i-blog.csdnimg.cn/blog_migrate/5d31d1b593a1fe197c09f88711583db9.jpeg)
public static void NetherlandsFlag (int[] arr, int num) {
int p1 = 0;
int p2 = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] < num) {
Swap(arr, p1, i);
p1++;
p2++;
}
else if (arr[i] == num) {
Swap(arr, p2, i);
p2++;
}
}
}
public static void Swap (int[] arr, int i, int j) {
if (i != j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
2.6 快速排序
左神讲的快排没有听懂,他找pivot是随机找的,速度更快。最后看的啊哈算法,快排,代码比较简洁。我写的代码把基准数换成数组的最后一位了。
![](https://i-blog.csdnimg.cn/blog_migrate/dfbc40cc043fc5e71ff5d92aa6d8f414.jpeg)
![快排变量设置](https://i-blog.csdnimg.cn/blog_migrate/af3d16a0b32078689987850cd6ab7e46.jpeg)
public void quickSort (int[] arr, int left, int right) {
if(left >= right) {
return;
}
int p1 = left; // 左下标
int p2 = right; // 右下标
int pivot = arr[right]; // 基准数, 最后一位
while (p1 < p2)
{
while (arr[p1] <= pivot && p1 < p2) // 先左
p1++;
while (arr[p2] >= pivot && p1 < p2) // 后右
p2--;
if(p1 < p2) {
swap(arr, p1, p2);
}
}
// 基准数归位
arr[right] = arr[p1];
arr[p1] = pivot;
quickSort(arr, left, p1 - 1); // 递归左边 // 这时候p1 == p2
quickSort(arr, p2 + 1, right); // 递归右边
}
2.7 希尔排序
![](https://i-blog.csdnimg.cn/blog_migrate/ed175965d15e50f7c7cf2f753405d8f6.png)
public static void shellSort (int[] arr) {
int length = arr.length;
int temp;
for (int gap = length / 2; gap >= 1; gap /= 2) {
for (int i = gap; i < length; i++) {
temp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > temp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = temp;
}
}
}
知道希尔排序的过程,但是代码写的太优雅了,读不懂,之后填坑。