一、插入排序
1. 直接插入排序
假定第一个元素有序,从第二个元素开始,把它放到 tmp,然后令 j 每次等于它前一个,然后依次跟 tmp 中的值进行比较,直到所有数据全部有序。
/**
* 时间复杂度:
* 最坏:O(N^2)
* 最好:O(N)
* 适合于:数据量不多,整体基本趋于有序
* 空间复杂度:O(1)
* 稳定性: 稳定
* 一个本身就稳定的排序,可以实现为不稳定的排序
* 但是一个本身就不稳定的排序 能实现为稳定的排序吗??
*
* @param array
*/
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
//array[j+1] = tmp;
break;
}
}
array[j + 1] = tmp;
}
}
2. 希尔排序(缩小增量排序)
希尔排序是直接排序的一个优化。采用分组的思想,通过多次分组,控制分组变量对每个组进行插入排序,而分组变量是逐次减少的,也就是缩小增量,这样可以让越大的数据越靠后,从而减小工作量。
/**
* 希尔排序
* O(N^1.3)
* 不是稳定的排序
*
* @param array
* @param gap
*/
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (array[j] > tmp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j + gap] = tmp;
}
}
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
gap /= 2;
shell(array, gap);
}
}
二、选择排序
1. 直接选择排序
选择排序的思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
首先用 i 下标遍历数组,minIndex 存储最小值的下标,然后用 j 下标从 i 后一个开始遍历,遇到比 minIndex 小的就更新 minIndex ,最后交换 i 下标和 minIndex下标的值。
/**
* 选择排序
* 时间复杂度:O(N^2) 和数据有序无序无关
* 空间复杂度:O(1)
* 稳定性: 不稳定
*
* @param array
*/
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
if (i != minIndex) {
swap(array, minIndex, i);
}
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
选择排序的另一个思路:
用两个变量存储最左边和最右边的元素,然后分别往中间靠拢,如果小就放到最左边,大就放到最右边。
public static void selectSort2(int[] array) {
int left = 0;
int right = array.length - 1;
while (left < right) {
int minIndex = left;
int maxIndex = left;
for (int j = left + 1; j <= right; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
if (array[j] > array[maxIndex]) {
maxIndex = j;
}
}
//最小值到前面
swap(array, minIndex, left);
//如果max下标正好是 left , 说明已经把最大值从 left 换到 minIndex 位置
if (maxIndex == left){
maxIndex = minIndex;
}
//最大值到后面
swap(array, maxIndex, right);
left++;
right--;
}
}
2. 堆排序
/**
* 堆排序
* 时间复杂度: O(n*logn)
* 空间复杂度: O(1)
* 稳定性:不稳定的算法
*
* @param array
*/
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shiftDown(array, 0, end);
end--;
}
}
public static void createBigHeap(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
private static void shiftDown(int[] array, int parent, int len) {
int child = (2 * parent) + 1;
while (child < len) {
if (child + 1 < len && array[child] < array[child + 1]) {
child++;
}
if (array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
三、交换排序
1. 冒泡排序
/**
* 冒泡排序
* 时间复杂度:(不考虑优化): O(n^2)
* 空间复杂度:O(1)
* 稳定性:稳定的排序
*
* @param array
*/
public static void bubbleSort(int[] array) {
//最外层控制趟数
for (int i = 0; i < array.length - 1; i++) {
boolean flg = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
flg = true;
}
}
if (flg == false) {
break;
}
}
}
2. 快速排序
(1)Hoare版
/**
* 快速排序
* 时间复杂度:O(N*logN)
* 空间复杂度:O(logN) 树的高度
* 稳定性:不稳定的排序
*
* @param array
* @param start
* @param end 问题:
* 当我们给定的数据 是有序的时候,这个快排的时间复杂度是O(n^2)
* 空间复杂度:O(n)
*/
private static void quick(int[] array, int start, int end) {
//注意大于号必须写,预防1 2 3 4 5 6 直接没有左树,或者没有右树
if (start >= end) {
return;
}
int pivot = partitionHoare(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
private static int partitionHoare(int[] array, int left, int right) {
int i = left;
int pivot = array[left];
while (left < right) {
//从左边开始找,必须先走右边。如果走左边,相遇之后的数据可能比基准大
while (left < right && //预防外部循环到相等后,后面都比基准大
array[right] >= pivot) { // 不能少等号,少了当有数据相等时会死循环
right--;
}
//right下标值小于pivot了
while (left < right &&
array[left] <= pivot) {
left++;
}
//left下标值大于pivot
swap(array, left, right);
}
//交换 和原来的left
swap(array, left, i);
return left;
}
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
(2)挖坑法
private static int partition(int[] array, int left, int right) {
int pivot = array[left];
while (left < right) {
while (left < right && array[right] >= pivot) {
right--;
}
array[left] = array[right]; //
while (left < right &&
array[left] <= pivot) {
left++;
}
array[right] = array[left]; //
swap(array, left, right);
}
array[left] = pivot;
return left;
}
(3)前后指针
private static int partitionPrevCur(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[left]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
3. 快速排序优化
问题:当数据有序的时候,快速排序的时间复杂度会达到最大,而且空间复杂度也会随之改变。
快速排序采用分而治之的思想,按照前面分析时间复杂度的时候有提到,一开始有图例所示数据,进行分而治之正好找到中间,
导致左边和右边的数据是均分的,那么最后的结果就可能会是接近于满二叉树的情况。
当按照上述所示方式进行划分时,效率就非常高,因为结果的 O(N*logN) 的推导就源自于此。
但是如果在划分的过程中数据一旦分布不均匀(比如有序时),就可能让子数据全部偏向一边,这样时间复杂度就会达到最高 O(n^2) 。
那么要怎么样才能达到均分的情况呢,这就用到第一种解决方式,三数取中法,这种方法可以对快速排序进行优化。
private static void quick(int[] array, int start, int end) {
if (start >= end) {
return;
}
System.out.println("start: " + start + " end: " + end);
//*在执行partition找基准之前,尽量解决划分不均匀的问题*//
int index = findMidValOfIndex(array, start, end);
swap(array, start, index);
int pivot = partitionPrevCur_3(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
//三数取中法
private static int findMidValOfIndex(int[] array, int start, int end) {
int midIndex = (start + end) / 2;
if (array[start] < array[end]) {
if (array[midIndex] < array[start]) {
return start;
} else if (array[midIndex] > array[end]) {
return end;
} else {
return midIndex;
}
} else {
if (array[midIndex] > array[start]) {
return start;
} else if (array[midIndex] < array[end]) {
return end;
} else {
return midIndex;
}
}
}
还可以采用另一种方式对快速排序进行进一步的优化,在递归到小的子区间时,考虑使用插入排序。
private static void quick(int[] array, int start, int end) {
if (start >= end) {
return;
}
if (end - start + 1 <= 15) {
//对start和end区间范围内使用插入排序
insertSort(array, start, end);
return;
}
//*在执行partition找基准之前,尽量解决划分不均匀的问题*//
int index = findMidValOfIndex(array, start, end);
swap(array, start, index);
int pivot = partitionPrevCur_3(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
private static void insertSort(int[] array, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= left; j--) {
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
//array[j+1] = tmp;
break;
}
}
array[j + 1] = tmp;
}
}
4. 快速排序非递归
public static void quickSortNotRecursive(int[] array) {
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length - 1;
int pivot = partition_2(array, start, end);
//1. 判断左边是否有两个元素
if (pivot > start + 1) {
stack.push(start);
stack.push(pivot - 1);
}
//2. 判断右边是否有两个元素
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
while (!stack.empty()) {
end = stack.pop();
start = stack.pop();
//3. 判断左边是否有两个元素
if (pivot > start + 1) {
stack.push(start);
stack.push(pivot - 1);
}
//4. 判断右边是否有两个元素
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
}
}
四、归并排序
1. 归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
核心步骤是将序列分解,然后将有序的子序列合并。
/**
* 归并排序
* 时间复杂度:O(N*logN)
* 空间复杂度:O(N)
* 稳定性:稳定
*
* @param array
*/
public static void mergeSort(int[] array) {
mergeSortChild(array, 0, array.length - 1);
}
private static void mergeSortChild(int[] array, int left, int right) {
if (left == right) {
return;
}
int mid = (left + right) / 2;
mergeSortChild(array, left, mid);
mergeSortChild(array, mid + 1, right);
//合并
merge(array, left, mid, right);
}
//用合并两个有序数组的方式处理!
private static void merge(int[] array, int left, int mid, int right) {
int s1 = left;
int e1 = mid;
int s2 = mid + 1;
int e2 = right;
int[] tmpArr = new int[right - left + 1];
int k = 0;
while (s1 <= e1 && s2 <= e2) {
if (array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
} else {
tmpArr[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
while (s2 <= e1) {
tmpArr[k++] = array[s2++];
}
//tmpArr当中的数据为有序的数据
for (int i = 0; i < k; i++) {
array[i + left] = tmpArr[i];
}
}
2. 非递归的归并排序
public static void mergeSortNotRecursive(int[] array) {
int gap = 1;
while (gap < array.length) {
for (int i = 0; i < array.length; i += gap * 2) {
int left = i;
int mid = left + gap - 1;
int right = mid + gap;
//注意处理越界问题
if (mid >= array.length) {
mid = array.length - 1;
}
if (right >= array.length) {
right = array.length - 1;
}
merge(array, left, mid, right);
}
gap *= 2;
}
}
3. 海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序的最常用的外部排序。
1. 先把文件切分成200分,每个512M
2. 分别对512M排序(把这512M数据读取到内存中,使用快排(或者其他的排序),写回文件当中),因为内存已经可以放的下,所以任意排序方式都可以
3. 进行2路归并,同时对200份有序文件做归并过程,最终结果就有序了