4排序算法
排序算法
1冒泡排序
1.1概念
- 冒泡排序(bubble sort)是一种基础的交换排序
- 思想:我们要把相邻的元素两两比较,当一个元素大于右侧元素时,交换它们的顺序;当一个元素小于或等于右侧相邻元素时,位置不变
- 冒泡排序是一种稳定排序,平均时间复杂度是O(n^2)
1.2冒泡排序第一版
public static void sort(int array[]){
for (int i=0;i<array.length-1;i++){
for (int j=0;j<array.length-i-1;j++){
int temp=0;
if (array[j]>array[j+1]){
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
}
1.3冒泡排序第二版
- 一轮中不交换了就直接停止循环
- 按照上面的逻辑,有序区的长度和排序的轮数是相等的,但实际上,数列真正的有序区可能会大于这个长度
public static void sort(int array[]){
for (int i=0;i<array.length-1;i++){
//有序标记,每一轮的初始值都是true
boolean isSorted=true;
//无序数列的边界,每次比较只需要比到这里为止
int sortBorder=array.length-1;
for (int j=0;j<sortBorder;j++){
int temp=0;
if (array[j]>array[j+1]){
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
//因为有元素进行交换,所以不是有序的,标记为false
isSorted=false;
//把无序数列的边界更新为最后一次交换元素的位置
sortBorder=j;
}
}
if (isSorted){
break;
}
}
}
1.4鸡尾酒排序
- 冒泡排序每一轮都是从左到右来比较元素,进行单向的位置交换
- 鸡尾酒排序的元素比较过程和交换过程都是双向的
public static void sort(int array[]){
int temp=0;
for (int i=0;i<array.length/2;i++){
//有序标记,每一轮的初始值都是true
boolean isSorted =true;
//奇数轮,从左向右比较和交换
for (int j=i;j<array.length-i-1;j++){
if (array[j]>array[j+1]){
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
isSorted=false;
}
}
if (isSorted){
break;
}
//在偶数轮之前,将isSorted重新标志为true
isSorted=true;
//偶数轮,从右向左比较和交换
for (int j=array.length-i-1;j<i;j--){
if (array[j]<array[j-1]){
temp=array[j];
array[j]=array[j-1];
array[j-1]=temp;
isSorted=false;
}
}
if (isSorted){
break;
}
}
}
- 鸡尾酒排序优点是能在特定条件下,减少排序的回合数,缺点就是代码量几乎增加了一倍
- 它能发挥优势的场景就是大部分元素以及有序的情况
2选择排序
2.1概述
- 每一轮选出最小元素交换到最左侧的思路就是选择排序
- 时间复杂度为O(n^2),空间复杂度为O(1),是不稳定的
2.2代码实现
public static void selectionSort(int[] array){
for (int i=0;i<array.length;i++){
int minIndex=i;
for (int j=i;j<array.length;j++){
if (array[j]<array[minIndex]){
minIndex=j;
}
}
if (i!=minIndex){
int temp=array[i];
array[i]=array[minIndex];
array[minIndex]=temp;
}
}
}
3插入排序
3.1概述
- 具体思路和扑克牌一样,在有序数组中插入元素
- 优化:每次插入时,不需要进行完整的交换,只需要把要插入元素暂时存起来,再把有序元素从前向后逐一复制
3.2代码
public static void insertSort(int[] array){
for (int i=1;i<array.length;i++){
int insertValue=array[i];
int j=i-1;
//从右向左比较元素的同时,进行元素复制
for (;(j>=0)&&(insertValue<array[j]);j--){
array[j+1]=array[j];
}
//insertValue的值插入适当位置
array[j+1]=insertValue;
}
}
4快速排序
4.1概述
- 快速排序也属于交换排序
- 思想:在每一轮挑选一个基准元素,并让其他比它的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分
- 总体时间复杂度是O(nlogn),空间复杂度是O(logn),是不稳定的
4.2基准元素的选择
- 基准元素pivot,在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边
- 最简单的方式就是选择数列的第一个元素
- 这样如果数列本身逆序就会形成极端情况,时间复杂读就变成O(n^2)
- 更好的是我们随机选择一个元素作为基准元素,并且让基准元素和数列首元素交换位置
4.3代码实现
- 单边循环法(选定基准元素pivot,同时设置一个mark指针指向数列起始位置,这个mark指针代表小于基准元素的区域边界)
- 双边循环法
public static void quickSort(int[] arr,int startIndex,int endIndex){
//递归结束条件:startIndex大于或等于endIndex
if (startIndex>=endIndex){
return;
}
//得到基准元素位置
int pivotIndex=partition(arr,startIndex,endIndex);
quickSort(arr,startIndex,pivotIndex-1);
quickSort(arr,pivotIndex+1,endIndex);
}
//分治,双边循环法
private static int partition(int[] arr,int startIndex,int endIndex){
//取第一个位置,(也可以选择随机位置)的元素作为基准元素
int pivot=arr[startIndex];
int left=startIndex;
int right=endIndex;
while (left!=right){
//控制right指针比较并左移
while (left<right && arr[right]>pivot){
right--;
}
//控制left指针比较并右移
while (left<right && arr[left]<=pivot){
left++;
}
//交换left和right指针所指向的元素
if (left<right){
int temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
}
}
//pivot和指针重合点交换
arr[startIndex]=arr[left];
arr[left]=pivot;
return left;
}
5希尔排序
5.1思想
- 逐步分组进行粗调,再进行直接插入排序的思路,就是希尔排序
- 比如8个元素,采用逐步折半的增量方法,分组跨度就是4,2,1,这种也被称为希尔增量
5.2代码
public static void shellSort(int[] array){
//希尔排序的增量
int d=array.length;
while (d>1){
//使用希尔增量的方式,即每次折半
d=d/2;
for (int x=0;x<d;x++){
for (int i=x+d;i<array.length;i=i+d){
int temp=array[i];
int j;
for (j=i-d;(j>=0)&&(array[j]>temp);j=j-d){
array[j+d]=array[j];
}
array[j+d]=temp;
}
}
}
}
5.3优化
- 因为每一轮希尔增量之间是等比的,这就导致了希尔增量存在盲区,于是有其他更严谨的增量方式
- Hibbard增量:1,3,7,15...通项公式就是2(k-1),这种最坏时间复杂度是O(n3/2)
- Sedgewick增量:1,5,19,41,109..通项公式就是(9 * 4^k-9 * 2^k + 1 ),这种最坏时间复杂度是O(n^4/3)
- 希尔排序是不稳定的
6堆排序
6.1概念
- 把无序数组构建成二叉堆,需要从小到大排序,则构建最大堆;需要从大到小排序,则构建最小堆
- 循环删除堆顶元素,替换成二叉堆的末尾,调整堆产生新的堆顶
- 时间复杂度是O(nlogn),空间复杂度是O(1),是不稳定的
6.2代码实现
//下沉调整
//array:要调整的堆 parentIndex:要下沉的父节点 length:堆的有效大小
public static void downAdjust(int[] array,int parentIndex,int length){
//temp保存父节点的值,用于最后的赋值
int temp=array[parentIndex];
int childIndex=2*parentIndex+1;
while (childIndex<length){
//如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if (childIndex+1<length && array[childIndex+1]>array[childIndex]){
childIndex++;
}
//如果父节点大于任何一个孩子的值,则直接跳出
if (temp>=array[childIndex]){
break;
}
array[parentIndex]=array[childIndex];
parentIndex=childIndex;
childIndex=2*childIndex+1;
}
array[parentIndex]=temp;
}
//堆排序(升序)
public static void heapSort(int[] array){
//1把无序数组构建成最大堆
for (int i=array.length/2;i>=0;i--){
downAdjust(array,i,array.length-1);
}
System.out.println(Arrays.toString(array));
//2循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
for (int i=array.length-1;i>0;i--){
int temp=array[i];
array[i]=array[0];
array[0]=temp;
//下沉,调整最大堆
downAdjust(array,0,i);
}
}
public static void main(String[] args) {
int[] array = {1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
heapSort(array);
System.out.println(Arrays.toString(array));
}
7归并排序
7.1概念
- 把大组不断对半分,分成小组,直到只剩两个,然后两两对比排序,最后归并成大组
- 归并操作需要三步
- 第一步:创建一个额外的大集合,用于存储归并结果,长度是两个小集合之和
- 第二步:从左到右逐一比较两个小集合中的元素,把较小的元素优先放入大集合
- 第三步:从另一个还有剩余元素的集合中,把剩余元素按顺序复制到大集合尾部
- 归并排序时间复杂度是O(nlogn),空间复杂度是O(n),是稳定的
7.2代码
public static void mergeSort(int[] array, int start, int end){
if(start<end){
//折半拆成两个小集合,分别进行扫描
int mid=(start+end)/2;
mergeSort(array,start,mid);
mergeSort(array,mid+1,end);
//把两个有序小集合,归并成一个大集合
merge(array,start,mid,end);
}
}
private static void merge(int[] array,int start,int mid,int end){
//开辟额外大集合,设置指针
int[] tempArray=new int[end-start+1];
int p1=start;
int p2=mid+1;
int p=0;
//比较两个小集合的元素,依次放入大集合
while ((p1<=mid) && (p2<=end)){
if (array[p1]<=array[p2]){
tempArray[p++]=array[p1++];
}
else {
tempArray[p++]=array[p2++];
}
}
//左侧小集合还有剩余,依次放入大集合尾部
while (p1<=mid){
tempArray[p++]=array[p1++];
}
//右侧小集合还有剩余,依次放入大集合尾部
while (p2<=end){
tempArray[p++]=array[p2++];
}
}
8计数排序
8.1概念
- 计数排序使用一个数组,该数组中每一个下标位置的值代表数列中对应整数出现的次数
- 适用于一定范围内的整数排序,在取值范围不是很大的情况下,它的性能甚至快过那些时间复杂度为O(nlogn)的排序
- 如果原始数列的规模是n,最大和最小整数的差值是m,则时间复杂度是O(n+m),空间复杂度是O(m)
8.2代码
public static int[] countSort(int[] array){
//1得到数列的最大值和最小值
int max=array[0];
int min=array[0];
for (int i=1;i<array.length;i++){
if (array[i]>max){
max=array[i];
}
if (array[i]<min){
min=array[i];
}
}
//2根据数列的最大值和最小值确定统计数组的长度
int[] countArray=new int[max-min+1];
//3遍历数组,填充统计数组
for (int i=0;i<array.length;i++){
countArray[array[i]]++;
}
//4遍历统计数组,输出结果
int index=0;
int[] sortedArray=new int[array.length];
for (int i=0;i<countArray.length;i++){
for (int j=0;j<countArray[i];j++){
sortedArray[index++]=i;
}
}
return sortedArray;
}
- 当数列最大和最小值差距过大时,并不适用计数排序
- 当数列元素不是整数时,也不适用计数排序
9桶排序
9.1概念
- 如一串数据:0.5,0.84,2.18,3.25,4.5
- 创建若干个桶,每个桶区有一定区间跨度
- 具体需要建立多少个桶,如何确定桶的区间范围,有很多种方式,这里创建桶的数量等于原始数列的元素数量。即5个,区间跨度=(最大值-最小值)/(桶的数量-1)
- 每个桶内部再使用归并排序
- 总体时间复杂度为O(n),空间复杂度为O(n)
- 桶排序性能并非绝对稳定,如果元素分布不均匀,极端情况下,第一个桶有n-1个元素,最后一个桶有1个元素,时间复杂度就退化为O(nlogn)
9.2代码
public static double[] buckSort(double[] array){
//1得到数列的最大值和最小值,并算出差值d
double max=array[0];
double min=array[0];
for (int i=1;i<array.length;i++){
if (array[i]>max){
max=array[i];
}
if (array[i]<min){
min=array[i];
}
}
double d=max-min;
//2初始化桶
//所有桶都保存在ArrayList集合中,每一个桶都被定义为一个链表LinkedList<Double> 这样便于在尾部插入元素
int bucketNum=array.length;
ArrayList<LinkedList<Double>> buckList = new ArrayList<>(bucketNum);
for (int i=0;i<bucketNum;i++){
buckList.add(new LinkedList<Double>());
}
//3遍历元素数组,将每个元素放入桶中
for (int i=0;i<array.length;i++){
int num=(int) ((array[i]-min)*(bucketNum-1)/d);
buckList.get(num).add(array[i]);
}
//4对每个桶内部进行排序
for (int i=0;i<buckList.size();i++){
//JDK底层采用了归并排序或归并排序的优化版
Collections.sort(buckList.get(i));
}
//5输出全部元素
double[] sortedArray = new double[array.length];
int index=0;
for (LinkedList<Double> list:buckList){
for (double element:list){
sortedArray[index]=element;
index++;
}
}
return sortedArray;
}
10基数排序
10.1概念
- 为了有效处理诸如手机号,英文单词等复杂元素的排序,仅仅靠一次技术排序很难实现,这时需要基数排序
- 基数排序把排序工作拆分成多个阶段,每一个阶段只根据一个字符进行计数排序,一共排序k轮(k是字符串的长度)
- 基数排序既可以从高位优先进行排序(MSD),也可以从低位优先进行排序(LSD)
- 如果排序的字符串长度不规则,就以最长字符串为准,其他不足的字符串在末尾补0就行了
- 如果原始数列的规模是n,最大和最小整数的差值是m,k是字符串的最大长度(也就是执行k次计数排序),时间复杂度是O(k(n+m)),空间复杂度是O(n+m)
11大总结
| 排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|
| 冒泡排序 | O(n^2) | O(1) | 稳定 |
| 鸡尾酒排序 | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(1) | 不稳定 |
| 插入排序 | O(n^2) | O(1) | 稳定 |
| 希尔排序 | O(n^4/3)... | O(1) | 不稳定 |
| 快速排序 | O(nlogn) | O(logn) | 不稳定 |
| 堆排序 | O(nlogn) | O(1) | 不稳定 |
| 归并排序 | O(nlogn) | O(n) | 稳定 |
| 计数排序 | O(n+m) | O(m) | 稳定 |
| 桶排序 | O(n) | O(n) | 稳定 |
| 基数排序 | O(k(n+m)) | O(n+m) | 稳定 |

浙公网安备 33010602011771号