快速排序-原理(不稳定)(重要)
- 从待排序区间选择一个数,作为基准值(baseValue);
- Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
- 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 =1,代表已经有序,或者小区间的长度= 0,代表没有数据。
partition操作:
- 可以选最后一个元素,也可以选最左边的元素,若果要选中间元素,就必须把这个中间元素交换到最前面或者,最后面。
- 若是选取最左边为基准值,先让right从右往左走找到比基准值小的数据,再让left从左到右走,找比基准值大的数据。
- 只要只是left下标小于right小标,交换left和right位置的元素。
- 若是选取最左边为基准值那么在left和right重合位置的下标的元素一定是小于基准值的。(原因代码里讲)
- 若是选取最右边为基准值那么在left和right重合位置的下标的元素一定是大于基准值的。(原因代码里讲)
- 接下来把重合位置的元素和基准值进行交换,在返回重合位置的下标。
- 再针对左右俩个区间,递归进行刚才的partition操作。
注意:
- 进行完一次partition操作之后,基准值左边所有元素一定是小于基准值的,基准值右边所有元素一定是大于基准值的。
- 若是选取最左边为基准值,那么一定先让right下标走,若是选取最右边为基准值,那么一定先让left下标先走。**
实现:
//快速排序
public void quickSort(int[] array) {
quickSortDeal(array, 0, array.length - 1);
}
private void quickSortDeal(int[] array, int left, int right) {
if(left >= right) {
return;
}
int index = partition(array,left,right);
quickSortDeal(array,left,index - 1);
quickSortDeal(array,index + 1, right);
}
private int partition(int[] array, int left, int right) {
//选取最左边为基准值
int baseVaule = array[left];
int leftIndex = left;
int rightIndex = right;
while (leftIndex < rightIndex) {
while (leftIndex < rightIndex && array[rightIndex] >= baseVaule) {
rightIndex --;
}
while (leftIndex < rightIndex && array[leftIndex] <= baseVaule) {
leftIndex ++;
}
if(leftIndex < rightIndex) {
int tmp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = tmp;
}
}
//此时leftIndex和rightIndex下标的元素一定小于或者等于我们选取的基准值
//若是因为rightIndex下标导致结束循环,那么rightIndex下标未找到比基准值小的数据,最后导致重合。
//若是因为leftIndex下标导致结束循环,那么说明rightIndex已经找到比基准值小的元素,最后重合与基准值交换。
int tmp = array[leftIndex];
array[leftIndex] = array[left];
array[left] = tmp;
return leftIndex;
}
性能分析:
- 时间复杂度
O(NlogN) - 空间复杂度
O(logN)
非递归实现快排:
思想:运用一个栈记住我们需要处理的left和right下标,再将下标出栈,进行partition操作。
实现:
//非递归实现快速排序
public void quickSortByLoop(int[] array) {
Stack<Integer> stack = new Stack<>();
//左边界先入栈
stack.push(0);
stack.push(array.length - 1);
while (!stack.isEmpty()) {
//右边界先出栈
int right = stack.pop();
int left = stack.pop();
if(left >= right) {
continue;
}
int index = partition(array, left,right);
//讲左边区间入栈,还要注意左边界先入栈
stack.push(left);
stack.push(index - 1);
//将右边区间入栈
stack.push(index + 1);
stack.push(right);
}
}
private int partition(int[] array, int left, int right) {
//选取最左边为基准值
int baseVaule = array[left];
int leftIndex = left;
int rightIndex = right;
while (leftIndex < rightIndex) {
while (leftIndex < rightIndex && array[rightIndex] >= baseVaule) {
rightIndex --;
}
while (leftIndex < rightIndex && array[leftIndex] <= baseVaule) {
leftIndex ++;
}
if(leftIndex < rightIndex) {
int tmp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = tmp;
}
}
//此时leftIndex和rightIndex下标的元素一定小于或者等于我们选取的基准值
//若是因为rightIndex下标导致结束循环,那么rightIndex下标未找到比基准值小的数据,最后导致重合。
//若是因为leftIndex下标导致结束循环,那么说明rightIndex已经找到比基准值小的元素,最后重合与基准值交换。
int tmp = array[leftIndex];
array[leftIndex] = array[left];
array[left] = tmp;
return leftIndex;
}
优化快速排序:
- 快排的性能受基准值的影响很大,如果我们选的基准值接近数组中的中位数,此时左右俩个区间较均衡性能也最好。
- 所以我们需要优化基准值,随机挑三个数字,取三个数字的中间值作为基准值。
实现:
//优化后的快速排序
public void betterQuickSort(int[] array) {
betterQuickSortDeal(array, 0, array.length - 1);
}
private void betterQuickSortDeal(int[] array, int left, int right) {
if(left >= right) {
return;
}
int index = betterQartition(array,left,right);
betterQuickSortDeal(array,left,index - 1);
betterQuickSortDeal(array,index + 1, right);
}
private int betterQartition(int[] array, int left, int right) {
int baseVaule = findBetterBaseVaule(array, left, right);
int leftIndex = left;
int rightIndex = right;
while (leftIndex < rightIndex) {
while (leftIndex < rightIndex && array[rightIndex] >= baseVaule) {
rightIndex --;
}
while (leftIndex < rightIndex && array[leftIndex] <= baseVaule) {
leftIndex ++;
}
if(leftIndex < rightIndex) {
int tmp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = tmp;
}
}
int tmp = array[leftIndex];
array[leftIndex] = array[left];
array[left] = tmp;
return leftIndex;
}
private int findBetterBaseVaule(int[] array, int left, int right) {
if(array.length < 3) {
return array[left];
}
int[] nums = new int[3];
nums[0] = array[left];
nums[1] = array[right];
Random rand = new Random();
//选取(left,right)下标中间的一个随机值
int ra = rand.nextInt(right) + left + 1;
nums[2] = array[ra];
Arrays.sort(nums);
for (int i = 0; i < array.length; i++) {
if(array[i] == nums[1]) {
array[i] = array[left];
array[left] = nums[1];
break;
}
}
return array[left];
}
- 还有优化方式就是当我们当前这个区间数组元素个数较少时,我们直接进行插入排序,
- 再就是当我们递归达到一定深度的时候,此时待排区间还有很多时,我们使用堆排序。
归并排序-原理(稳定)(重要)
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
实现:
//归并排序
public void mergeSort(int[] array) {
//归并排序区间采用左闭右开格式,快排采用左闭右闭格式
mergeSortHealper(array, 0, array.length);
}
private void mergeSortHealper(int[] array, int left, int right) {
if(right - left <= 1) {
return;
}
int mid = (left + right) / 2;
mergeSortHealper(array,left,mid);
mergeSortHealper(array,mid,right);
merge(array,left,mid,right);
}
//将俩个有序序列合并为一个有序序列
private void merge(int[] array, int left, int mid, int right) {
int leftIndex = left;
int rightIndex = mid;
int[] tmpNums = new int[right - left];
int tmpIndex = 0;
while (leftIndex < mid && rightIndex < right) {
if(array[leftIndex] <= array[rightIndex]) {
tmpNums[tmpIndex] = array[leftIndex];
tmpIndex ++;
leftIndex ++;
}else {
tmpNums[tmpIndex] = array[rightIndex];
tmpIndex ++;
rightIndex ++;
}
}
while (leftIndex < mid) {
tmpNums[tmpIndex] = array[leftIndex];
tmpIndex ++;
leftIndex ++;
}
while (rightIndex < right) {
tmpNums[tmpIndex] = array[rightIndex];
tmpIndex ++;
rightIndex ++;
}
for (int i = 0; i < right - left; i++) {
array[left + i] = tmpNums[i];
}
}
性能分析:
-时间复杂度
O(NlogN)
-空间复杂度
O(N)
非递归实现:
思想:首先将gap=1也就是此时一组一个元素进行相邻排序合并,再将gap=2进行分组合并,依次类推,直到gap=array.length
实现:
//归并排序的非递归实现
public void mergeSortByLoop(int[] array) {
//gap表示多少个元素一组
for (int gap = 1; gap < array.length; gap *= 2) {
for (int left = 0; left < array.length; left += 2 * gap) {
int mid = left + gap;
int right = mid + gap;
//防止数据越界访问
if(mid > array.length) {
mid = array.length;
}
if(right > array.length) {
right = array.length;
}
merge(array,left,mid,right);
}
}
}
//将俩个有序序列合并为一个有序序列
private void merge(int[] array, int left, int mid, int right) {
int leftIndex = left;
int rightIndex = mid;
int[] tmpNums = new int[right - left];
int tmpIndex = 0;
while (leftIndex < mid && rightIndex < right) {
if(array[leftIndex] <= array[rightIndex]) {
tmpNums[tmpIndex] = array[leftIndex];
tmpIndex ++;
leftIndex ++;
}else {
tmpNums[tmpIndex] = array[rightIndex];
tmpIndex ++;
rightIndex ++;
}
}
while (leftIndex < mid) {
tmpNums[tmpIndex] = array[leftIndex];
tmpIndex ++;
leftIndex ++;
}
while (rightIndex < right) {
tmpNums[tmpIndex] = array[rightIndex];
tmpIndex ++;
rightIndex ++;
}
for (int i = 0; i < right - left; i++) {
array[left + i] = tmpNums[i];
}
}
处理大数据思想:
外部排序(本质上还是学习归并排序细想):排序过程需要在磁盘等外部存储进行的排序。
前提:内存只有 1G,需要排序的数据有 100G。
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序。
- 先把文件切分成 200 份,每个 512 M。
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以。
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。