目录
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作.
常见的排序算法有插入排序(直接插入排序和希尔排序),选择排序(选择排序和堆排序),交换排序(冒泡排序和快速排序)以及归并排序.
我们将从时间复杂度,空间复杂度,以及排序的稳定性来分析这七大排序.
排序的稳定性
假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序是稳定的.
直接插入排序
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列 。
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 {
break;
}
}
array[j+1] = tmp;
}
}
时间复杂度:考虑最坏的情况下,就是全逆序的时候,此时时间复杂度为O(N^2).
最好的情况下,有序的时候,此时时间复杂度为O(N).得出一个结论:当数据量不多,且数据基本上是趋于有序的时候,此时直接插入排序是非常快的.
空间复杂度:O(1)
稳定性:稳定.一个本身就稳定的排序,可以实现为不稳定的排序;但是一个本身不稳定的排序,不能实现为稳定的排序.
希尔排序
希尔排序(缩小增量排序)是直接插入排序的一个优化.
基本思想:先将数据进行分组,将每一组内的数据先进行排序(这一过程叫做预排序);逐渐缩小组数,直到最后整体看作是一组,采用直接插入排序进行排序.
跳着分组的原因:尽可能的使小的数据靠前,大的数据靠后,从而使整体更加趋于有序.
public static void shellSort(int[] array){
int gap = array.length;
while (gap > 1){
gap /= 2;
shell(array,gap);
}
}
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 {
break;
}
}
array[j+gap] = tmp;
}
}
当gap>1时,都是预排序,目的是让数组更接近于有序.当gap==1时,此时数组已经接近有序了,这样进行插入排序就会很快.
希尔排序的时间复杂度不好计算,因为gap的取值方法有很多,导致很难去计算.目前还没有证明gap具体取多少是最快的.
时间复杂度:O(N^1.3),估计的时间复杂度.
空间复杂度:O(1)
稳定性:不稳定.
直接选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放到序列的起始位置,直到全部排序的数据元素排完.
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[minIndex] > array[j]){
minIndex = j;
}
}
//处理两个下标一样的情况
if (i != minIndex) {
int tmp = array[i];
array[i] = array[minIndex];
array[minIndex] = tmp;
}
}
}
直接选择排序好理解,但是效率低下.
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
堆排序
排升序要建大堆,排降序建小堆.
升序,建大堆:堆顶元素和最后一个元素交换,将数组长度-1,在对堆顶元素进行向下调整,依次循环.
java
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--;
}
}
//建立大根堆
//从最后一颗子树的根节点开始,每一次都进行向下调整
private 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;
}
}
}
public static void swap(int[] array,int x,int y){
int tmp = array[x];
array[x] = array[y];
array[y] = tmp;
}
时间复杂度:O(n*logn)
空间复杂度:O(1)
稳定性:不稳定
冒泡排序
相邻元素之间的比较.
java
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;
}
}
}
时间复杂度:(不考虑优化)O(n^2)
空间复杂度:O(1)
稳定性:稳定
快速排序
快速排序是Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
public static void quick(int[] array,int start,int end){
//大于号 是预防1 2 3 4 5 6,直接没有左树或没有右树
if (start >= end){
return;
}
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
//找基准
//Hoare版本
private static int partition(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和right相遇
//交换 和原来的left
swap(array,left,i);
//返回基准
return left;
}
时间复杂度:O(n*logn)
此时间复杂度不是最坏情况下的时间复杂度,最坏情况下是有序的情况下,此时树的高度是n,时间复杂度是O(n^2),空间复杂度也变成了O(n).
空间复杂度:O(logn)
稳定性:不稳定
挖坑法找基准
java
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];
}
array[left] = pivot;
//返回基准
return left;
}
挖坑法是找到合适的值直接交换.
需要注意的是挖坑法和hoare找基准的结果是不一样的但是最终都是有序的.
快速排序优化
当数据有序的时候,快速排序的时间复杂度达到最大,而且空间复杂度也会随之改变.
三数取中法
为了使二叉树的划分尽可能的均匀,我们在left,mid,right三个数中,取出中间大的值,来作为key(left).
比如1 2 3 4 5,如果不采用三数取中法,那么1作为key走下来,left和right在1相遇,基准就是1,此时划分的就是单分支的树;如果采用三数取中,将数组顺序调整为 3 2 1 4 5,3作为key,走下来,left和right在中间位置相遇,将3和1交换,变为 1 2 3 4 5,虽然又变回去了,但是此时的基准在中间位置3的地方,此时二叉树划分的将更加均匀.
java
public static void quick(int[] array,int start,int end){
//大于号 是预防1 2 3 4 5 6,直接没有左树或没有右树
if (start >= end){
return;
}
//在找基准之前,进行三数取中的优化,尽量去解决划分不均匀的问题.
//在left,mid,right三个数中找到中间大的数字做key
int index = findMidValueOfIndex(array, start, end);
swap(array,start,index);
int pivot = partitionHoare(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
//3个数中取中位数
private static int findMidValueOfIndex(int[] array,int start,int end){
int minIndex = (start+end)/2;
if (array[start] > array[end]){
if (array[minIndex] > array[start]){
return start;
}else if (array[minIndex] < array[end]){
return end;
}else {
return minIndex;
}
}else {
if (array[minIndex] > array[end]){
return end;
}else if (array[minIndex] < array[start]){
return start;
}else {
return minIndex;
}
}
}
我们除了采取这种优化之外,还可以在快速排序递归到小区间的时候,采用插入排序.
因为插入排序在数据趋于有序并且数据量小的时候,排序的速度非常快.
非递归实现快速排序
java
public static void quickSort2(int[] array) {
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length-1;
int pivot = partition(array,start,end);
//1.判断左边是不是有2个元素
if(pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
//2.判断右边是不是有2个元素
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
while (!stack.isEmpty()) {
end = stack.pop();
start = stack.pop();
pivot = partition(array,start,end);
//3.判断左边是不是有2个元素
if(pivot > start+1) {
stack.push(start);
stack.push(pivot-1);
}
//4.判断右边是不是有2个元素
if(pivot < end-1) {
stack.push(pivot+1);
stack.push(end);
}
}
归并排序
先分解,后合并.
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并.
java
public static void mergeSort1(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;//表示tmpArr 的下标
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 <= e2) {
tmpArr[k++] = array[s2++];
}
//tmpArr当中 的数据 是right left 之间有序的数据
for (int i = 0; i < k; i++) {
array[i+left] = tmpArr[i];
}
}
时间复杂度:O(n*logn).
空间复杂度:O(n)
稳定性:稳定
非递归的归并排序
java
public static void mergeSort(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;
}
}