1.排序
1.1 稳定性
举例:
注:一个本身就稳定的排序,可以实现不稳定,但是一个不稳定的排序,不能实现稳定.
1.2 常见的排序算法
2.常见排序算法的实现
2.1 插入排序
2.1.1 基本思想
- 把待排序的数据按其大小逐个插入到一个已经排好序的有序序列中,直到所有的数据插入完为止,得到 一个新的有序序列 (就像玩扑克牌一样,把接下来接的牌,插入到已经排好序的扑克牌中,直到所有的牌插入完为止)
2.1.2 直接插入排序
/**
* 时间复杂度:
* 最好情况下:O(n) -> 数据有序的情况下 1 2 3 4 5
* 最坏情况下:O(n^2) -> 数据逆序的情况下 5 4 3 2 1
* 空间复杂度:O(1)
* 稳定性:稳定的排序
* 当数据越有序的时候 直接插入排序的效率越高
*/
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.1.3 希尔排序(缩小增量排序)
实际上是跳跃式分组:
//每组进行直接插入排序
private 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 {
//array[j+gap] = tmp;
break;
}
}
array[j+gap] = tmp;
}
}
/**
* 时间复杂度:O(n^1.25 - n^1.5) -> n^1.3
* 空间复杂度:O(1)
* 稳定性:不稳定排序
*/
public static void shellSort(int[] array) {
int gap = array.length;//7
while (gap > 1) {
gap /= 2;//1
shell(array,gap);
}
//shell(array,1);
}
2.2 选择排序
2.2.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.2.2 直接选择排序
/**
* 选择排序
* 时间复杂度: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;
}
}
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 i = left+1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array,minIndex,left);
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array,maxIndex,right);
left++;
right--;
}
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
2.2.3 堆排序
/**
* 堆排序
* 时间复杂度: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,end,0);
siftDown(array,0,end);
end--;
}
}
private static void createBigHeap(int[] array) {
for (int i = (array.length-1-1) / 2; i >= 0 ; i--) {
siftDown(array,i,array.length);
}
}
private static void siftDown(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;
}
}
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
2.3 交换排序
2.3.1 基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.2 冒泡排序
/**
* 冒泡排序
* 时间复杂度:O(N^2) 对数据不敏感 有序 无序都是这个复杂度!
* 空间负责度:O(1)
* 稳定性:稳定的排序
* 插入排序 冒泡排序
*
* 加了优化之后,时间复杂度可能会变成O(n)
* @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) {
break;
}
}
}
2.3.3 快速排序
2.3.3.1 Hoare版
/**
* hoare法
*/
private static int parttion(int[] array,int left,int right) {
int i = left;//记录这个位置
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
//todo: 先检查前面的会不会有问题
while (left < right && array[left] <= tmp) {
left++;
}
swap(array,left,right);
}
swap(array,left,i);
return left;
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
int pivot = parttion(array,start,end);
quick(array,start,pivot-1);//左树
quick(array,pivot+1,end);//右树
}
/**
* 时间复杂度:
* O(n*logN)[最好情况了] O(N^2)[数据是有序的 或者是逆序的 ]
* 空间复杂度:O(logN)[好的情况] O(n) [不好的情况]
* 稳定性:不稳定排序
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
2.3.3.2 挖坑法
/**
* 挖坑法
*/
private static int parttion(int[] array,int left,int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
array[left] = array[right];
//todo: 先检查前面的会不会有问题
while (left < right && array[left] <= tmp) {
left++;
}
array[right] = array[left];
}
array[left] = tmp;
return left;
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
int pivot = parttion(array,start,end);
quick(array,start,pivot-1);//左树
quick(array,pivot+1,end);//右树
}
/**
* 时间复杂度:
* O(n*logN)[最好情况了] O(N^2)[数据是有序的 或者是逆序的 ]
*
* 空间复杂度:O(logN)[好的情况] O(n) [不好的情况]
* 稳定性:不稳定排序
* @param array
*/
public static void quickSort(int[] array) {
quick(array,0,array.length-1);
}
2.3.3.3 前后指针
看着代码理解即可
private static int partition(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
2.3.4 快速排序优化
2.3.4.1 三数取中法选key
/**
* 三数取中法
* @param array
* @param left
* @param right
* @return 返回中间数字的下标
*/
private static int threeNum(int[] array,int left,int right) {
int mid = (left+right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] < array[right]) {
return right;
}else if(array[mid] > array[left]) {
return left;
}else {
return mid;
}
}
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
//三数取中
int mid = threeNum(array,start,end);
//交换
swap(array,mid,start);
int pivot = parttion(array,start,end);
quick(array,start,pivot-1);//左树
quick(array,pivot+1,end);//右树
}
2.3.4.2 递归到小的子区间时,可以考虑使用插入排序
private static void insertSort2(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;
}
}
private static void quick(int[] array,int start,int end) {
if(start >= end) {
return;
}
if(end - start +1 <= 20) {
//直接插入排序
insertSort2(array,start,end);
return;
}
//三数取中
int mid = threeNum(array,start,end);
//交换
swap(array,mid,start);
int pivot = parttion(array,start,end);
quick(array,start,pivot-1);//左树
quick(array,pivot+1,end);//右树
}
2.3.5 快速排序非递归
/**
* 非递归实现快速排序
*/
public static void quickSort1(int[] array) {
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length-1;
if(end - start +1 <= 20) {
//直接插入排序
insertSort2(array,start,end);
return;
}
//三数取中
int mid = threeNum(array,start,end);
//交换
swap(array,mid,start);
int pivot = parttion(array,start,end);
if(pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.empty()) {
end = stack.pop();
start = stack.pop();
if(end - start +1 <= 20) {
//直接插入排序
insertSort2(array,start,end);
//return; 这个地方不能return
//只排了一个区间,剩下的还没排
}else {
mid = threeNum(array,start,end);
//交换
swap(array,mid,start);
pivot = parttion(array, start, end);
if (pivot > start + 1) {
stack.push(start);
stack.push(pivot - 1);
}
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
}
}
}
private static void insertSort2(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;
}
}
2.4 归并排序
2.4.1 基本思想
- 归并排序是建立在归并操作上的一种有效的排序算法,采用分治法.
- 将已有序的子序列合并,得到完全有序的序列. (即先使每个子序列有序,再使子序列段间有序)
- 若将两个有序表合并成一个有序表,称为二路归并。
2.4.2 归并排序
先分解再合并
/**
* 时间复杂度:O(N*logN)
* 空间复杂度:O(N)
* 稳定性:稳定的排序
*/
public static void mergeSort1(int[] array) {
mergeSortFunc(array,0,array.length-1);
}
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;//tmpArr数组的下标
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]){
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
//s2++;
//k++;
}
}
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < k; i++) {
array[i+left] = tmpArr[i];
}
}
private static void mergeSortFunc(int[] array,int left,int right) {
if(left >= right) {
return;
}
int mid = (left+right) / 2;
mergeSortFunc(array,left,mid);
mergeSortFunc(array,mid+1,right);
merge(array,left,mid,right);//合并
}
2.4.3 非递归实现归并排序
public static void mergeSort(int[] array) {
int gap = 1;
while (gap < array.length) {
for (int i = 0; i < array.length; i = i + gap*2) {
int left = i;
int mid = left+gap-1;
int right = mid+gap;
//mid 和 right 有可能会越界
if(mid >= array.length) {
//纠正
mid = array.length-1;
}
if(right >= array.length) {
right = array.length-1;
}
merge(array,left,mid,right);
}
gap *= 2;
}
}
2.4.3 海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
3.排序算法复杂度及稳定性分析
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 空间复杂度 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔排序 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 不稳定 |
快速排序 | O(n * log(n)) | O(n * log(n)) | O(n^2) | O(log(n)) ~ O(n) | 不稳定 |
归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(1) | 稳定 |
4.其他非基于比较排序(了解)
4.1 计数排序
4.1.1 思想
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
4.2.2 操作步骤
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
4.3.3 特性总结
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定