1.概述
1.1 概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.2 常见应用
- 高校排行
- 商品排序
2. 常见排序
2.1 直接插入排序
从序列的第二个位置开始为基准,把前面的一个元素依次与下一个元素比较,前一个元素大于后一个元素(升序),则交换,如果碰到小于的,则证明前几个元素已经有序了,因为是从第二个元素开始进行插入排序的,直接break掉.
动态演示:
/**
* 插入排序
* @param array 传入的数组
*/
public static void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {//基准从1开始
for (int j = i-1; j >= 0; j--) {
if (array[j] > array[j+1]){//把前面的元素依次比较
swap(j,j+1,array);
}else {
break;//遇到不符合条件的直接break
}
}
}
}
private static void swap(int a,int b,int[] array){
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
特性总结:
- 元素越趋于有序,插入排序越快,因为遇到比较不符合条件的直接回break掉.
- 时间复杂度:两层循环,O(n2)
- 空间复杂度:只在数组本身上进行了操作,O(1)
- 稳定性:稳定
2.2 希尔排序
希尔排序实际上是直接插入排序的一种升级版,先把序列中的元素按照一定的间隔个数分成多个组,之后把各个组中的元素进行排序,之后缩小缩小间隔个数,也就是缩小组数,增大各个组中的元素个数,再次排序,直到元素之间的间隔小于1为止.
动态演示:
/**
* 希尔排序,本质上是插入排序的一种升级版
* @param array
*/
public static void shellSort(int[] array){
int gap = array.length;
while (gap > 1){//数据间隙大于1时继续希尔排序
gap = gap/2;//缩小数据间隙
shell(array,gap);
}
}
private static void shell(int[] array,int gap){
for (int i = gap; i < array.length; i++) {//针对每一组数据进行插入排序
for (int j = i-gap; j >= 0; j-=gap) {//只不过每次的步数变成了gap
if (array[j] > array[j+gap]){
swap(j,j+gap,array);
}else {
break;
}
}
}
}
其实我们观察上述代码,我们为什么说他是直接插入排序的升级版,是因为,它相比直接插入排序只是改变了每次走的步数.
特性总结:
- 希尔排序是针对直接插入排序的优化
- 当gap>1时都是预排序,目的是让数据越来越接近有序.当gap==1的时候,就是直接插入排序,这时数据已经趋于有序了,直接插入排序就会特别快.
- 时间复杂度:目前仍然存在争议,一般认为是O(n1.25~1.6n1.25)
- 稳定性:不稳定
2.3 选择排序
从第一个元素开始,寻找比基准元素小的元素,下标保存到tmp中,之后继续寻找比tmp小的元素,直到找到最小的元素为止,之后交换基准元素和tmp下标的元素,基准后移,一些此类推.
动态演示:
/**
* 选择排序
* @param array
*/
public static void selectSort(int[] array){
for (int i = 0; i < array.length; i++) {
int min = i;//最小值的下标赋值为i,如果没有找到比min下标小的,说明i就是最小的
int j = i+1;
for (; j < array.length; j++) {
if (array[min] > array[j]){
min = j;
}
}
swap(min,i,array);
}
}
特性总结:
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 这种排序方法虽然好理解,但是由于效率太低,日常开发中很少用.
2.4 冒泡排序
之所以叫冒泡排序,是因为就像水中的气泡一样,一点一点地浮出水面.先确定排序的趟数(下标最大值),就是外层循环,再确定排序交换的次数(下标最大值-i),就是内层循环,遇到比自己小的就交换,这样最大值就像冒泡一样排到了最后.
优化: 如果发现在内层循环遍历的时候,一次都没有交换,说明已经有序,我们通过flag来标记.
动态演示:
/**
* 冒泡排序
* @param array
*/
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1; i++) {//如果遍历到最后一个元素,就没有必要比较了,就要-1
boolean flag = true;
for (int j = 0; j < array.length-i-1; j++) {//这里必须-1,因为如果不-1,就会遍历到最后一个元素,j+1就会越界
if (array[j] > array[j+1]){
swap(j+1,j,array);
flag = false;//发生了交换.说明不有序,设置为false
}
}
if (flag){//如果flag == true,说明没有发生交换,整个数组已经有序
return;//直接返回
}
}
}
特性总结:
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
- 稳定性:稳定
2.5 堆排序
首先如果是升序的话,先创建一个大根堆,让end指向最后一个元素为什么不创建小根堆呢?是因为小根堆只可以保证父节点是最小的,而不可以保证子节点是从小到大排列的.之后把堆顶元素和堆尾元素交换,就把整个堆的最大元素排到了最后.之后继续把该堆向下调整为大根堆,之后让end向前移动
动态演示:
/**
* 堆排序,从小到大排序,创建大根堆
* @param array
*/
public static void heapSort(int[] array){
int end = array.length-1;
createBigHeap(array);//先创建大根堆
while (end > 0){
swap(0,end,array);//交换堆顶元素和最后一个元素,把堆顶最大的元素放在最后
end--;//end向前移动
shiftDown(0,end,array);//再次向下调整为大根堆
//先向下调整,再--,因为向下调整的时候,while循环没有等号,如果传入end-1,倒数第二个结点就调整不到
}
}
private static void createBigHeap(int[] array){
for (int i = (array.length-1-1)/2; i >= 0 ; i--) {
shiftDown(i,array.length-1,array);
}
}
private static void shiftDown(int parent,int end,int[] array){
int child = parent*2+1;
while (child <= end){
if (child+1 < end && array[child] < array[child+1]) {
child++;
}
if (array[child] > array[parent]){
swap(parent,child,array);
parent = child;
child = child*2+1;
}else {
break;
}
}
}
特点总结:
- 堆排序适用于海量数据排序,数据越多,堆排序效率越高.
- 时间复杂度:复杂度主要集中于向下调整中,元素个数x树的高度,O(n*log2n)
- 空间复杂度:没有额外空间,O(1)
- 稳定性:不稳定
2.6 快速排序(重点)
之所以叫快速排序,说明它是真的快.快速排序整体思想为分治思想,就是把通过递归的思想把整个数组通过一定的方法切成二叉树的形式,之后对每科子树进行排序.
快速排序的方法有两种,一种是霍尔法,一种是挖坑法.
2.6.1 霍尔法
分别定义start和end指向数组首和尾,选取第一个元素为key,之后先让end向前移动,找到比key小的元素,之后再让start向后移动,找到比key大的元素,之后交换end和start下标的值,以此类推.直到start和end相遇,把相遇位置的元素和key交换,之后以相遇点分治,递归以此类推.
动态演示:
/**
* 快速排序,整体思想:把小的往前放,把大的往后放
* @param array
*/
public static void quickSort(int[] array){
quick(array,0,array.length-1);//规定开始和结束位置
}
private static void quick(int[] array,int s,int e){
int left = s;
int right = e;
if (left >= right){//当左下标大于右下标的时候,说明递归完成,直接返回
return;
}
swap(left,mid,array);//把区间内的中间值放在头
int pos = position(left,right,array);//通过霍尔法找到分治点
quick(array,left,pos-1);
quick(array,pos+1,right);//以分制点为基准,向两侧递归
}
/**
* 霍尔法找到相遇位置
* @param start 开始位置
* @param end 结束位置
* @param array 原始数组
* @return
*/
private static int position(int start,int end,int[] array){
int tmp = array[start];
int i = start;//提前准备比较数据的下标
while (start < end){
while (start < end && array[end] >= tmp){//先走后面,注意加上等号
end--;
}
while (start < end && array[start] <= tmp){//加前一个条件是为了防止出现已经排好序的极端情况
start++;
}
swap(start,end,array);//当在前面找到比tmp大的数据,在后面找到比tmp大的数据,就交换
}
swap(i,start,array);//当start和end相遇的时候,交换数列头元素和相遇地方的元素
return start;//返回相遇的点,以相遇的点分制
}
2.6.2 挖坑法
先把key(首)元素下标放入tmp中,让end向前移动,找到比tmp小的元素,填上key元素的坑,之后让start向后移动找到比tmp大的元素,填上上一个end的坑.以此类推,最后让tmp填上start和end相遇位置的坑.之后分治,递归重复上述操作.
/**
* 快速排序,整体思想:把小的往前放,把大的往后放
* @param array
*/
public static void quickSort(int[] array){
quick(array,0,array.length-1);//规定开始和结束位置
}
private static void quick(int[] array,int s,int e){
int left = s;
int right = e;
if (left >= right){//当左下标大于右下标的时候,说明递归完成,直接返回
return;
}
swap(left,mid,array);//把区间内的中间值放在头
int pos = position2(left,right,array);
quick(array,left,pos-1);
quick(array,pos+1,right);//以分制点为基准,向两侧递归
}
/**
*挖坑法找相遇位置
* @param start 开始位置
* @param end 结束位置
* @param array 原始数组
* @return
*/
private static int position2(int start,int end,int[] array){
int tmp = array[start];
while (start < end){
if (start < end && array[end] >= tmp){//先走后面,否则下面没法填坑
end--;
}
array[start] = array[end];
if (start < end && array[start] <= tmp){
start++;
}
array[end] = array[start];
}
array[start] = tmp;
return start;
}
2.6.3 快速排序优化
- 由于快速排序越来越趋向有序,所以我们可以以分治之后数组的长度作为基准,当小于一定的值之后,就可以对分治区域使用插入排序.
- 为了防止出现二叉树单分支的情况而降低效率,所以我们需要在分治区间找到中间大小的元素,与首元素交换.
快速排序最终版:
/**
* 快速排序,整体思想:把小的往前放,把大的往后放
* @param array
*/
public static void quickSort(int[] array){
quick(array,0,array.length-1);//规定开始和结束位置
}
private static void quick(int[] array,int s,int e){
int left = s;
int right = e;
if (left >= right){//当左下标大于右下标的时候,说明递归完成,直接返回
return;
}
//优化2:在霍尔法或者挖坑法使得数组不断趋于有序时,我们就可以发挥直接插入排序的优势
//越趋于有序,越快
if (right-left < 7){
insertSort2(left,right,array);
}
//优化1:寻找中间大小的数字,防止出现单分支情况导致效率太低
int mid = findmid(array,left,right);
swap(left,mid,array);//把区间内的中间值放在头
int pos = position(left,right,array);
quick(array,left,pos-1);
quick(array,pos+1,right);//以分制点为基准,向两侧递归
}
private static void insertSort2(int start,int end,int[] array){
for (int i = start+1; i <= end; i++) {
for (int j = i-1; j < end; j++) {
if (array[j] > array[j+1]){
swap(j,j+1,array);
}else {
break;
}
}
}
}
//比较区间内的开始,中间,结束位置的值
private static int findmid(int[] array,int start,int end){
int mid = (start+end)/2;//区间内的中间位置
//先比较左右开始和结束的值
if (array[start] < array[end]){
if (array[end] < array[mid]){//mid插入它们两个之间的三个空隙
return end;
} else if (array[mid] > array[start]) {
return mid;
}else {
return start;
}
}else {
if (array[start] < array[mid]){
return start;
} else if (array[end] < array[mid]) {
return mid;
}else {
return end;
}
}
}
/**
* 霍尔法找到相遇位置
* @param start 开始位置
* @param end 结束位置
* @param array 原始数组
* @return
*/
private static int position(int start,int end,int[] array){
int tmp = array[start];
int i = start;//提前准备比较数据的下标
while (start < end){
while (start < end && array[end] >= tmp){//先走后面,注意加上等号
end--;
}
while (start < end && array[start] <= tmp){//加前一个条件是为了防止出现已经排好序的极端情况
start++;
}
swap(start,end,array);//当在前面找到比tmp大的数据,在后面找到比tmp大的数据,就交换
}
swap(i,start,array);//当start和end相遇的时候,交换数列头元素和相遇地方的元素
return start;//返回相遇的点,以相遇的点分制
}
/**
*挖坑法找相遇位置
* @param start 开始位置
* @param end 结束位置
* @param array 原始数组
* @return
*/
private static int position2(int start,int end,int[] array){
int tmp = array[start];
while (start < end){
if (start < end && array[end] >= tmp){//先走后面,否则下面没法填坑
end--;
}
array[start] = array[end];
if (start < end && array[start] <= tmp){
start++;
}
array[end] = array[start];
}
array[start] = tmp;
return start;
}
2.7 归并排序
先把数组从中间拆分开,让每个小数组有序,之后把小数组合并回来,大数组就是有序的.也是分治思想,整体也是一棵二叉树的形状.
动态演示:
/**
* 归并排序
* @param array
*/
public static void mergSort(int[] array){
mergFunc(0,array.length-1,array);
}
private static void mergFunc(int left,int right,int[] array){
//拆分数组
int mid = (left+right)/2;
if (left >= right){//递归到left>=right的时候,直接返回
return;
}
//向两侧递归
mergFunc(left,mid,array);
mergFunc(mid+1,right,array);
//开始合并
merg(left,right,mid,array);
}
//在合并的时候进行排序
private static void merg(int left,int right,int mid,int[] array){
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
int k = 0;
int [] tmp = new int[right-left+1];
while (s1 <= e1 && s2 <= e2){
if (array[s1] < array[s2]){
tmp[k] = array[s1];
k++;
s1++;
}else {
tmp[k] = array[s2];
k++;
s2++;
}
}
//看哪个数组还有数据,拷贝进去
while (s1 <= e1){
tmp[k] = array[s1];
k++;
s1++;
}
while (s2 <= e2){
tmp[k] = array[s2];
k++;
s2++;
}
//把临时数组中的数据拷贝到原数组中
for (int i = 0; i < tmp.length; i++) {
array[left+i] = tmp[i];
}
}
特性总结:
- 时间复杂度:元素个数x数的高度,O(n*log2n)
- 空间复杂度:额外申请了一个数组,所以是O(n)
- 稳定性:稳定