活动地址:CSDN21天学习挑战赛
学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您:
想系统/深入学习某技术知识点…
一个人摸索学习很难坚持,想组团高效学习…
想写博客但无从下手,急需写作干货注入能量…
热爱写作,愿意让自己成为更好的人…
…
1.堆排序
通过建立一个大顶堆,依次取出大顶堆元素放到末尾,再次建堆(分治)
- 排序函数:
void sort(int[] nums)
- 恢复函数:
void siftDown(int[] nums, int parent, int heapSize)
1.1 算法分析
选择排序可以看作是冒泡排序的优化,而堆排序则可以看作是选择排序的优化,其减少了选择的次数,步骤如下:
首先对序列进行原地建堆(heapify)然后重复执行以下操作,直到堆的元素数量为 1
- 交换堆顶元素与尾元素
- 堆的元素数量减 1
- 对 0 位置进行 1 次 siftDown(下滤) 操作
1.2 算法实现
public void sort(int[] nums) {
// 建堆(自低向上,下滤)
int heapSize = nums.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(nums, i, heapSize);
}
// 依次取出堆顶,放到数组尾部
while (heapSize > 1) {
heapSize--;
int tmp = nums[heapSize];
nums[heapSize] = nums[0];
nums[0] = tmp;
siftDown(nums, 0, heapSize);
}
}
public void siftDown(int[] nums, int parent, int heapSize) {
int element = nums[parent];
// 最后一个非叶子下标
int end = (heapSize >> 1) - 1;
while (parent <= end) {
// 寻找最大孩子元素
int childI = parent * 2 + 1;
int childV = nums[childI];
if (childI + 1 < heapSize && nums[childI + 1] > nums[childI]) {
childI = childI + 1;
childV = nums[childI];
}
// 如果最大孩子大于父元素,则将孩子上移,否则结束循环
if (childV > element) {
nums[parent] = childV;
parent = childI;
} else {
break;
}
}
nums[parent] = element;
}
1.3 复杂度分析
- 时间复杂度:首先建堆的时间复杂度为O(n),其次需要进行大约n次的时间复杂度为O(1)的获取堆顶元素操作,而每次获取堆顶元素后都需要进行下滤操作,而下滤操作的时间复杂度为O(logn),所以总体的时间复杂度为O(n) + O(n) * (O(1) + O(logn)) = O(nlogn)
- 空间复杂度:没有引入新的空间,直接将选出的最值放到原先数组的末尾,所以为O(1)
1.4 细节分析
上面的建堆采用的是自下而上的下滤方式建堆,除此之外,其实还有一种自上而下的上滤建堆方式,只不过其建堆的时间复杂度会由原先的O(n)上升到O(nlogn),所以没有采用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5gVeu6j-1660461290931)(img/heapify.png)]
2.快速排序
通过选取的某个数,将数据分成两部分,一部分小于这个数,一部分大于这个数,直到不能分为止(分治)
排序函数:
void sort(int[] nums)
快速排序:
void sort(int[] nums, int left, int right)
返回轴点:
int pivotIndex(int[] nums, int left, int right)
2.1 算法分析
快速排序可以说是我们最常用的一种排序算法了,其运用了分治的思想,平均每次减少一半的复杂度。首选,快速排序会选出一个数作为轴点,可以选择数组的最左边元素,也可以随机的选择一个元素,这里推荐随机的选择一个元素,因为如果使用最左边元素作为轴点可能出现最差情况(数组原先是完全正/逆序排列)。
快速排序的核心就是确定轴点元素的位置,上面我们已经随机获取到了一个轴点值。首先,将其与最左边元素交换位置,然后保存轴点元素值,之后从右开始与这个轴点元素进行比较,如果比这个轴点元素大,则继续向左移动,如果比这个轴点元素小,则将此元素覆盖轴点元素并开始在从左边向右扫描,如果比轴点元素小,则继续向左移动,如果比轴点元素大,则将值覆盖上次右侧的指针指向的元素,直到左右指针重合,则该点就是轴点元素的位置。
2.2 算法实现
public void sort(int[] nums) {
sort(nums, 0, nums.length);
}
/**
* [left,right)的快速排序
*/
private void sort(int[] nums, int left, int right) {
if (right - left < 2) return;
int pivotIndex = pivotIndex(nums, left, right);
sort(nums, left, pivotIndex);
sort(nums, pivotIndex + 1, right);
}
/**
* 快排核心操作,返回轴点位置
*/
private int pivotIndex(int[] nums, int left, int right) {
// 选取轴点,如果为 pivot[left]则容易分摊不均
E pivot = nums[left + new Random().nextInt(right - left)];
// 将右指针指向右边最后一个元素
right--;
while (left < right) {
// 从右边向左扫描
while (left < right) {
if (pivot < nums[right]) {
right--;
} else {
nums[left++] = nums[right];
break;
}
}
// 从左边开始向右扫描
while (left < right) {
if (pivot > nums[left]) {
left++;
} else {
nums[right--] = nums[left];
break;
}
}
}
nums[left] = pivot;
return left;
}
2.3 复杂度分析
- 时间复杂度:最好(平均)时间复杂度O(nlog),最坏时间复杂度O(n^2)
- 空间复杂度:没有新的额外空间引入O(1)
3.归并排序
归并排序,类似于快速排序,只不过其每次都是均匀的从中间分割元素
排序函数:
void sort(int[] nums)
递归排序:
void sort(int[] nums, int left, int right)
合并操作:
merge(int begin, int endLeft, int endRight)
3.1 算法分析
归并排序,是将一个大数组拆分为两个小数组,使两个小数组有序后在进行组合,这样每次递归都会减少一半的数据量
3.2 算法实现
protected void sort(int[] nums) {
sort(nums,0, data.length);
}
/**
* 从[left,right)排序
*/
private void sort(int[] nums, int left, int right) {
if (right - left < 2) return;
int mid = (left + right) >> 1;
sort(left, mid);
sort(mid, right);
merge(left, mid, right);
}
/**
* 左侧[left, mid),右侧[mid,endRight)
*/
private void merge(int[] nums, int begin, int endLeft, int endRight) {
// 创建临时数组,用于copy
E[] tmp = (E[]) new Object[endRight - begin];
int curIndex = 0;
// 左右数组指针
int p1 = begin;
int p2 = endLeft;
while (p1 < endLeft && p2 < endRight) {
// 为保证稳定性,一定要为 > 不能是 >=
if (data[p1] > data[p2]) {
tmp[curIndex++] = nums[p2++];
} else {
tmp[curIndex++] = nums[p1++];
}
}
// 处理剩余元素
while (p1 < endLeft) {
tmp[curIndex++] = nums[p1++];
}
while (p2 < endRight) {
tmp[curIndex++] = nums[p2++];
}
System.arraycopy(tmp, 0, data, begin, endRight - begin);
}
3.3 时间复杂度分析
- 时间复杂度:由于每次都是均分,所有时间复杂度为O(nlogn)
- 空间复杂度:需要n/2的空间存储临时数据,所以空间复杂度为o(n)