您现在的位置是:首页 >技术交流 >排序【Java】网站首页技术交流
排序【Java】
排序的相关概念
排序的概念
排序:即对于一串数据,按照一定的规则(递增或递减)进行调整,使其变为有序的操作;
排序算法的稳定性
排序算法的稳定性指对于一串记录中相同的记录,在经过排序后,对于这些相同记录的内部次序保持不变,就称该算法是稳定的;
常见的排序算法
插入排序
插入排序又称为直接插入排序,该排序算法的基本思想在于:将需要进行排序的数据根据其大小逐一插入到一组已经有序的数据中,所有待排序的数据插入完毕,则排序完毕;在对一组数据进行初次排序时,认为第一个数是有序的;
算法流程:
- 首次插入时默认第一个元素有序,从第2个元素开始与其前面的元素进行比较,当前进行比较的元素记为tmp;
- 找到比tmp小的值,直接将tmp插入到该元素之后;
- 重复上面的操作,直到整个序列有序;
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(1);
时间复杂度:最坏情况下为O(n^2);最好情况下为O(n);
最坏情况下即数据逆序时;
关于直接插入排序是否是一个稳定的算法:
如果是上面所示的代码,通过模拟就可以发现其是稳定的,但若我们尝试将其比较操作修改为 if (array[j]>=tmp) ,再次模拟算法执行的流程就会发现其又是不稳定的,但我们依然认为插入排序是一种稳定的排序算法;
如果一个排序本身就是稳定的,我们可以通过一定的操作使其成为不稳定的;
如果一个排序本身就是不稳定的,没有办法可以使其成为一个稳定的排序算法;
直接插入排序的使用场景:
更多地使用于数据量小,且趋于有序的数据集合中;
希尔排序
希尔排序又称为缩小增量排序,该排序算法的基本思想是:首先选定一个数gap,将待排序的数据分成n/gap个组,所有距离为gap的数据为一组,分别针对每一组进行插入排序,gap依据一定的规则不断减小,直到gap=1,针对整个数据进行插入排序即可;
算法流程:
- 将整个待排序的序列分成若干个组,距离为gap的元素为一组;
- 分别针对每一组的元素进行排序;
- gap的值不断减小,直到gap=1;
- 所有元素最终有序;
代码实现:
public static void shell(int [] arr,int gap){
for (int i=gap;i<arr.length;i++){
int tmp=arr[i];
int j=i-gap;
for (;j>=0;j-=gap){
if (arr[j]>tmp){
arr[j+gap]=arr[j];
}else{
break;
}
}
arr[j+gap]=tmp;
}
}
public static void shellSort(int [] arr){
int gap=arr.length;
while (gap>1){
shell(arr,gap);
gap/=2;
}
shell(arr,1);
}
当gap>1时,所进行的排序实际都是一种预排序,主要是为了让数据集合不断趋于有序,最终当gap=1是,实际进行的就是直接插入排序了;
希尔排序的空间复杂度和时间复杂度:
空间复杂度:O(1);
时间复杂度:O(n^1.25) ~ O(1.6n ^1.25);
关于希尔排序的时间复杂度,由于gap的取值方法有多种,即增量的变化方式不同,也有许多科学家提出了不同的取值方式,因此时间复杂度也就难以计算,上面给出的时间复杂度的范围是由Knuth得出的;
希尔排序是一种不稳定的排序算法;
选择排序
选择排序的基本思想是:每一次都从待排序的数据中选择最小的一个元素放在数据集合的起始位置,重复这样的操作直到整个数据集合全部有序;
算法流程:
- 以第一个元素为基准,从它之后的一个元素开始寻找比基准小的元素,找到就交换该元素与当前基准的位置;
- 基准向后移动,重复上面的操作;
- 直到整个数组有序;
代码实现:
public static void selectSort(int [] arr){
for (int i=0;i<arr.length;i++){
int minIndex=i;
for (int j=i+1;j<arr.length;j++){
if (arr[j]<arr[minIndex]){
minIndex=j;
}
}
swap(arr,minIndex,i);
}
}
private static void swap(int [] arr,int i,int j){
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
时间复杂度和空间复杂度:
空间复杂度:O(1);
时间复杂度:O(n^2);
选择排序是一种不稳定的排序算法;
堆排序
堆排序实际也是选择排序的一种,是基于堆这种数据结构提出的一种排序方法;进行堆排序的前置工作是建堆,一般从小到大排建大堆,从大到小排建小堆;建堆完成以后通过向下调整的方式使数据逐渐有序;
代码实现:
/*
* 向下调整
* */
private static void shiftDown(int [] arr,int root,int len){
int parent=root;
int child=(2*parent)+1;
while (child<len){
if (child+1<len&&arr[child]<arr[child+1]){
child++;
}
if (arr[child]>arr[parent]){
swap(arr,child,parent);
parent=child;
child=2*parent+1;
}else {
break;
}
}
}
/*
* 建堆
* */
private static void createHeap(int [] arr){
for (int p=(arr.length-1-1)/2;p>=0;p--){
shiftDown(arr,p,arr.length);
}
}
public static void heapSort(int [] arr){
createHeap(arr);
int end=arr.length-1;
while (end>=0){
swap(arr,0,end);
shiftDown(arr,0,end);
end--;
}
}
private static void swap(int [] arr,int i,int j){
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
堆排序的时间复杂度和空间复杂度:
空间复杂度:O(1);
时间复杂度:O(n*logn);(以2为底)
堆排序是一种不稳定的排序算法;
冒泡排序
冒泡排序属于是一种交换排序,主要思想就是比较相邻2个元素的大小,较大值向后移动,较小值向前移动;
代码实现:
public static void bubbleSort(int arr[]){
for (int i=0;i<arr.length-1;i++){
for (int j=0;j<arr.length-1-i;j++){
if (arr[j]>arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
冒泡排序算法可以进行一定的优化:
public static void bubbleSort2(int arr[]){
for (int i=0;i<arr.length-1;i++){
boolean flg=false;
for (int j=0;j<arr.length-1-i;j++){
if (arr[j]>arr[j+1]){
swap(arr,j,j+1);
flg=true;
}
}
if (!flg){
break;
}
}
}
冒泡排序的时间复杂度和空间复杂度:
空间复杂度:O(1);
时间复杂度:O(n^2);
冒泡排序是一种稳定的排序算法;
快速排序
快速排序是一种利用了二叉树结构的交换排序,它的基本思想是:以待排序序列中的任一元素为基准值,通过前后元素的不断交换,使左边序列中的所有元素均小于基准值,右边序列中的所有元素均大于基准值,最终使所有元素有序;
前面提到快速排序是利用了二叉树结构的排序,因此实现快速排序同样可以借助二叉树前序遍历的递归思想来实现:
public static void quickSort(int [] arr,int left,int right){
//不满足递归条件,递归结束
if (left>=right) return;
//使用partition()方法得到基准值
int pivot=partition(arr,left,right);
//对左边序列递归排序
quickSort(arr,left,pivot-1);
//对右边序列递归排序
quickSort(arr,pivot+1,right);
}
所以实现快速排序的核心就是实现基准值的选取,通过将区间按照基准值划分成左右两部分的方式即可完成排序,常见的划分方法主要有以下几种:
- Hoare法
public static int partitionHoare(int [] arr,int low,int high){
//首先记录当前的默认基准值以及所处的下标位置
int i=low;
int tmp=arr[low];
while (low<high){
//从右向左寻找比基准值小的数
while (low<high&&arr[high]>=tmp){
high--;
}
//从左向右寻找比基准值大的数
while (low<high&&arr[low]<=tmp){
low++;
}
//交换两边找到的值
swap(arr,low,high);
}
//此时low与high相遇,与基准值进行交换
swap(arr,low,i);
//返回新的基准值
return low;
}
- 挖坑法
public static int partitionHole(int [] arr,int low,int high){
//记录下当前基准的值
int tmp=arr[low];
while (low<high){
//从右向左寻找比基准值小的数
while (low<high&&arr[high]>=tmp){
high--;
}
//使用当下找到的值填之前的坑
arr[low]=arr[high];
//从左向右寻找比基准值大的数
while (low<high&&arr[high]<=tmp){
low++;
}
//使用当下找到的满足条件的值去填之前找到的值留下的坑
arr[high]=arr[low];
}
//此时low和high相遇,用前面记录的基准值填当下的坑
arr[low]=tmp;
//返回新的基准值
return low;
}
- 双指针法
public static int partitionPointer(int [] arr,int low,int high){
//第一个指针指向当前基准的下一个值
int d=low+1;
//记录当前基准的值
int pivot=arr[low];
//i为第2个指针,与第1个指针同位置开始
for (int i=low+1;i<=high;i++){
//当当前指针指向的值小于基准值,交换2个指针所指向的值
if (arr[i]<pivot){
swap(arr,i,d);
d++;
}
}
swap(arr,d-1,low);
return d-1;
}
快速排序实际是运用了分而治之的思想,当待排序的序列以基准值为准,分成均比基准值大的一部分和均比基准值小的一部分,再对两部分分别进行排序 。但如若是在数据逆序但有序的极端情况下,待排序数据的结构类似于一棵单分支的数,此时若依然使用上面的排序方法,排序效率就难免低下,因此我们需要对普通的快速排序做出优化:
- 融合插入排序进行优化
对于数据量不大且趋于有序的待排序序列而言,使用插入排序无疑是一种不错的方法,所以我们可以使用插入排序来避免过多次的数据交换和移动,从而提高排序效率;
public static void quickSort(int [] arr,int left,int right){
//不满足递归条件,递归结束
if (left>=right) return;
//当数据量小于一定值时,使用插入排序
if(right-left+1<=60000){
insertSort(arr,left,right);
}
//使用partition()方法得到基准值
int pivot=partitionPointer(arr,left,right);
//对左边序列递归排序
quickSort(arr,left,pivot-1);
//对右边序列递归排序
quickSort(arr,pivot+1,right);
}
- 随机选取基准法
依然是为了避免排序数据的结构为单分支的情况,假设基准值的下标为left,就从[left+1,right]这样一个区间随机获取一个下标,与left下标的数据进行交换,这种方式基本就可以避免出现单分支的情况。但随机选取基准法同样也存在一个问题,即随机就代表了不确定性,难免不会出现
更多的问题;
- 三数取中法
关于快速排序的基准值的选取,过大或过小都会影响排序的效率,三数取中法的三数即序列的首元素,尾元素以及中间位置的数,取中即选取三个数中第二大的数;
private static int medOfThreeIndex(int [] arr,int left,int right){
//得到中间位置的值
int mid=left+((right-left)>>>1);
//选择第二大的数返回
if (arr[left]<arr[right]){
if (arr[mid]<arr[left]){
return left;
}else if(arr[mid]>arr[right]){
return right;
}else {
return mid;
}
}else {
if (arr[mid]<arr[right]){
return right;
}else if(arr[mid]<arr[left]){
return left;
}else {
return mid;
}
}
}
关于上面的几种优化方法,融合插入排序和随机选取基准的方法实际都是对区间内比较的优化,但三数取中法则是对序列本身分割的优化,可以有效减少排序过程中递归的深度;
上面快速排序的实现以及优化方法实际都是利用递归思想完成的,下面是快速排序非递归方法的实现(借助栈来实现):
private static void quickSortNo(int [] arr){
Stack<Integer> stack=new Stack<>();
int left=0;
int right=arr.length-1;
int pivot=partitionHole(arr,left,right);
if (pivot>left+1){
stack.push(left);
stack.push(pivot-1);
}
if (pivot<right-1){
stack.push(pivot+1);
stack.push(right);
}
while (!stack.isEmpty()){
right=stack.pop();
left=stack.pop();
pivot=partitionHole(arr,left,right);
if (pivot>left+1){
stack.push(left);
stack.push(pivot-1);
}
if (pivot<right-1){
stack.push(pivot+1);
stack.push(right);
}
}
}
非递归的实现思想实际也是通过将序列区间的下标放到栈中,再借助之前寻找基准的方法确定基准值,再对区间进行划分,重复这样一个操作来完成。
快速排序的时间复杂度与空间复杂度:
空间复杂度:
好的情况下:O(logn)
坏的情况下:O(n) 单分支结构
时间复杂度:
好的情况下:O(n*logn)
坏的情况下: O(n^2) 有序或逆序
快速排序是一种不稳定的排序;
归并排序
归并排序同样是一种采用了分治思想的排序算法,其核心思想就是首先将待排序序列划分成若干个子序列,先使每个子序列有序,再将已经有序的子序列进行合并,最终得到完全有序的序列;
下面是具体的代码实现:
private static void merge(int [] arr,int low,int mid,int high){
//记录被分开的2个序列的开始位置坐标和结束位置坐标
int s1=low;
int e1=mid;
int s2=mid+1;
int e2=high;
//创建一个与原数组等大的临时数组
int [] tmpArr=new int[high-low+1];
//k用来表示临时数组的下标
int k=0;
//进行排序以及合并
//表示2个数组段内都有数据
while (s1<=e1&&s2<=e2){
//选择较小的数放到临时数组中
if (arr[s1]<=arr[s2]){
tmpArr[k++]=arr[s1++];
}else {
tmpArr[k++]=arr[s2++];
}
}
//此时至少存在有一个段的数组为空,直接将剩余的元素全部放到临时数组中
while (s1<=e1){
tmpArr[k++]=arr[s1++];
}
while (s2<=e2){
tmpArr[k++]=arr[s2++];
}
//将排序好的数据放到原数组中
for (int i=0;i<tmpArr.length;i++){
arr[i+low]=tmpArr[i];
}
}
private static void mergeSortInternal(int [] arr,int low,int high){
//递归退出的条件,此时只有一个元素或没有元素
if (low>=high) return;
//找到序列的中间位置
int mid=low+((high-low)>>>1);
//以中间位置为基准,分成左右2个序列,再分别递归
mergeSortInternal(arr,low,mid);
mergeSortInternal(arr,mid+1,high);
//进行排序并合并
merge(arr,low,mid,high);
}
public static void mergeSort(int [] arr){
mergeSortInternal(arr,0,arr.length-1);
}
上面归并排序的实现方法是基于递归实现的,下面是其非递归的代码实现:
public static void mergeSortNo(int [] arr){
//gap代表划分的子序列中的元素个数
int gap=1;
while (gap<arr.length){
for (int i=0;i<arr.length;i+=2*gap){
//left代表子序列的第一个元素
int left=i;
int mid=left+gap-1;
//防止越界
if (mid>=arr.length){
mid=arr.length-1;
}
//被划分的子序列的最后一个元素
int right=mid+gap;
if (right>=arr.length){
right=arr.length-1;
}
//进行排序合并
merge(arr,left,mid,right);
}
//扩大子序列
gap*=2;
}
}
归并排序的时间复杂度与空间复杂度:
时间复杂度:O(n*logn);
空间复杂度:O(n)
归并排序是一种稳定的排序;
上面常见的七种排序算法虽然各不相同,但却都是基于数据之间的比较来实现的,下面是几种非基于比较的排序算法。
其他排序算法
计数排序
计算排序是通过统计相同元素出现的次数来达到排序目的的算法;
算法流程:
以一组数据值都在0~9范围内的待排序序列为例:
- 首先创建一个大小为10的数组array,数组下标的范围与数据值的范围一致;
- 遍历待排序的序列,每遍历到一个数据,就将与该数据相等的array数组下标位置的值加1,即当遍历到数字5,就将array数组的5下标对应的元素加一;直到序列遍历结束;
- 最后遍历array数组,数组的下标对应的元素为几,就打印几次该位置的下标;即若0下标的元素值为2,就打印2个0;
具体的代码实现:
public static void countSort(int [] arr){
//通过待排序序列的最大值和最小值确定数组下标需要的范围
//1.获取序列的最大值与最小值
int maxVal=arr[0];
int minVal=arr[0];
for (int i=1;i<arr.length;i++){
if (arr[i]<minVal){
minVal=arr[i];
}
if (arr[i]>maxVal){
maxVal=arr[i];
}
}
//2.开始计数
int range=maxVal-minVal+1;
int [] count=new int[range];
for (int i=0;i<arr.length;i++){
count[arr[i]-minVal]++;
}
//遍历计数数组,将数据放回原数组
int index=0;
for (int i=0;i<arr.length;i++){
while (count[i]>0){
arr[index++]=i+minVal;
count[i]--;
}
}
}
计数排序的时间复杂度和空间复杂度与待排序序列的范围有关,一般相对于更集中的数据使用该算法效果更好;
基数排序
基数排序同样是一种非比较型的排序,其核心思想是将待排序的整数按其位数(个位,十位,百位等)进行分割,再按照每个位数进行比较;