目录
O(n^2)
- 选择排序是不稳定的,冒泡排序、插入排序是稳定的;
- 在这三个排序算法中,选择排序交换的次数是最少的;
- 在数组几乎有序的情况下,插入排序的时间复杂度接近线性级别。
冒泡
一边比较一边向后两两交换,将最大值 / 最小值冒泡到最后一位;
经过优化的写法:使用一个变量记录当前轮次的比较是否发生过交换,如果没有发生交换表示已经有序,不再继续排序;
public static void bubbleSort(int[] arr) {
// 初始时 swapped 为 true,否则排序过程无法启动
boolean swapped = true;
for (int i = 0; i < arr.length - 1; i++) {
// 如果没有发生过交换,说明剩余部分已经有序,排序完成
if (swapped==false) break;
// 设置 swapped 为 false,如果发生交换,则将其置为 true
swapped = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 如果左边的数大于右边的数,则交换,保证右边的数字最大
swap(arr, j, j + 1);
// 表示发生了交换
swapped = true;
}
}
}
}
// 交换元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
选择
简单选择排序
选择排序的思想是:双重循环遍历数组,每经过一轮比较,找到最小元素的下标,将其交换至首位
public static void selectionSort(int[] arr) {
int minIndex;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
// 记录最小值的下标
minIndex = j;
}
}
// 将最小元素交换至首位
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
二元选择排序
class Solution {
public int[] sortArray(int[] nums) {
//二元选择排序
//同时找最大最小值
int min;
int max;
for(int i=0;i<nums.length/2;i++){
min=i;
max=i;
//获取最大最小值下标
for(int j=i+1;j<nums.length-i;j++){
if(nums[j]<=nums[min]){
min=j;
}
if(nums[j]>nums[max]){
max=j;
}
}
//当max=min时,必定为max=min=i,且[i,length-i]都等于nums[i]
if(min==max) break;
//交换最小值
swap(nums,i,min);
//当max=i时,此时i的值已经交换到min处
if(max==i) max=min;
//交换最大值
swap(nums,nums.length-i-1,max);
}
return nums;
}
//交换
public void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
插入
在新数字插入过程中,与前面的数字不断比较,前面的数字不断向后挪出位置,当新数字找到自己的位置后,插入一次即可。
class Solution {
public int[] sortArray(int[] nums) {
if(nums.length<2) return nums;
//交换法插入排序
//从第二个数开始
for(int i=1;i<nums.length;i++){
int cur=nums[i];
//插入位置之前的数字
int j=i-1;
//把比插入数大的数都移到后面
while(j>=0 && nums[j]>cur){
//插入的数比它前面的数小
//把前面的数移到后面
nums[j+1]=nums[j];
j--;
}
// 两种情况会跳出循环:
//1. 遇到一个小于或等于cur的数字,跳出循环,cur就坐到它后面。
//2. 已经走到数列头部,仍然没有遇到小于或等于cur的数字,也会跳出循环,此时j等于 0,cur就坐到数列头部。
nums[j+1]=cur;
}
return nums;
}
}
O(nlogn)
希尔排序
本质是插入排序,时间复杂度由经验得出o(n^1.25)-o(1.6n^1.25)
public void shellSort(int[] nums){
//间隔序列,在希尔排序中我们称之为增量序列
for(int gap=nums.length/2;gap>0;gap/=2){
//插入排序
//从gap开始,按照顺序将每个元素依次向前插入自己所在的组
for(int i=gap;i<nums.length;i++){
//cur站起来,开始找位置
int cur=nums[i];
//该组前一个数字的索引
int j=i-gap;
while(j>=0 && nums[j]>=cur){
nums[j+gap]=nums[j];
j-=gap;
}
//cur插入找到了自己的位置,坐下
nums[j+gap]=cur;
}
}
}
堆排序
class Solution {
//堆排序
public int[] sortArray(int[] nums) {
//建立初始大根堆
buildMaxHeap(nums);
//调整大根堆
for(int i=nums.length-1;i>0;i--){
//将大根堆顶与最后一个元素交换,通过不断交换,最后得到一个升序数组
swap(nums,0,i);
// 调整剩余数组,使其满足大顶堆
maxHeapify(nums, 0, i);
}
return nums;
}
//建立初始大根堆
public void buildMaxHeap(int[] nums){
//从第一个非叶子节点开始
for(int i=nums.length/2-1;i>=0;i--){
//调整每一个子树为大根堆
maxHeapify(nums,i,nums.length);
}
}
//调整大根堆,第二个参数为堆顶,第三个参数为,参与调整的最后一个数的下标+1
public void maxHeapify(int[] nums,int i,int heapSize){
//左子树
int l=2*i+1;
//右子树
int r=l+1;
//记录根结点、左子树结点、右子树结点三者中的最大值下标
int largest=i;
// 与左子树结点比较
if(l<heapSize && nums[l]>nums[largest]){
largest=l;
}
// 与右子树结点比较
if(r<heapSize && nums[r]>nums[largest]){
largest=r;
}
if(largest!=i){
// 将最大值交换为根结点
swap(nums,i,largest);
// 再次调整交换数字后的大顶堆
maxHeapify(nums,largest,heapSize);
}
}
//交换
public void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
快速排序
class Solution {
//快排
public int[] sortArray(int[] nums) {
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort(int[] nums,int start,int end){
//如果区域内的数字少于2个,退出递归
if(start>=end) return;
//获得轴
int middle=getPivot(nums,start,end);
//递归
quickSort(nums,start,middle-1);
quickSort(nums,middle+1,end);
}
// 将nums从start到end分区,左边区域比基数小,右边区域比基数大,然后返回中间值的下标
public int getPivot(int[] nums,int start,int end){
//取第一个数为基数
int pivot=nums[start];
//左边界
int left=start+1;
//右边界
int right=end;
//移动两边界
while(left<right){
while(left<right && nums[left]<=pivot) left++;
while(left<right && nums[right]>=pivot) right--;
//此时左边界>pivot,右边界<pivot
//交换左右边界
if(left<right){
swap(nums,left,right);
left++;
right--;
}
}
//此时两种情况,left==right left>right
//单独判断left==right,nums[right]>pivot情况
if(left==right && nums[right]>pivot){
right--;
}
//此时都是left>right,轴right将数组分成两边界(start,right],(right,end]
//将基数和轴交换
swap(nums,start,right);
return right;
}
//交换方法
public void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
归并排序
class Solution {
//归并排序
public int[] sortArray(int[] nums) {
//临时数组result
int[] result=new int[nums.length];
//归并排序
mergeSort(nums,0,nums.length-1,result);
//此时nums与result相同
return result;//此时nums与result相同
}
// 对 nums 的 [start, end] 区间归并排序
public void mergeSort(int[] nums,int start,int end,int[] result){
// 只剩下一个数字,停止拆分
if(start==end) return;
int middle=(start+end)/2;
// 拆分左边区域,并将归并排序的结果保存到 result 的 [start, middle] 区间
mergeSort(nums,start,middle,result);
// 拆分右边区域,并将归并排序的结果保存到 result 的 [middle + 1, end] 区间
mergeSort(nums,middle+1,end,result);
// 合并左右区域到 result 的 [start, end] 区间
merge(nums,start,end,result);
}
// 将 nums 的 [start, middle] 和 [middle + 1, end] 区间合并
public void merge(int[] nums,int start,int end,int[] result){
//分割
int middle=(start+end)/2;
// 数组 1 的首尾位置
int start1=start;
int end1=middle;
// 数组 2 的首尾位置
int start2=middle+1;
int end2=end;
// 用来遍历数组的指针
int index1=start1;
int index2=start2;
// 结果数组的指针
int resultIndex=start1;
//比较插入结果数组
while(index1<=end1 && index2<=end2){
if(nums[index1]<=nums[index2]){
result[resultIndex++]=nums[index1++];
}else{
result[resultIndex++]=nums[index2++];
}
}
// 将剩余数字补到结果数组之后
while(index1<=end1){
result[resultIndex++]=nums[index1++];
}
while(index2<=end2){
result[resultIndex++]=nums[index2++];
}
// 将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较
for(int i=start;i<=end;i++){
nums[i]=result[i];
}
}
}
O(n)
计数排序(倒序遍历计数数组)
class Solution {
//计数排序(倒序遍历计数数组)
public int[] sortArray(int[] nums) {
//判空及越界
if(nums==null || nums.length<=1) return nums;
//最大最小值
int min=0;
int max=0;
for(int num:nums){
if(num<min) min=num;
if(num>max) max=num;
}
//计数
int range=max-min+1;
int[] counting=new int[range];
for(int element:nums){
counting[element-min]++;
}
//记录每种元素最后一个的下标
//此处记录的下标,需要用记录的数字-1
counting[0]--;
for(int i=1;i<range;i++){
//位置 = 前面比自己小的数字的总数 + 自己的数量 - 1
//由于counting[0]已经减了1,所以后续的减1可以省略
counting[i]+=counting[i-1];
}
//计数排序
int[] result=new int[nums.length];
//从后往前遍历数组,通过counting中记录的下标位置,将nums中的元素放到result数组中
for(int i=nums.length-1;i>=0;i--){
result[counting[nums[i]-min]]=nums[i];
//更新 counting[nums[i] - min],指向此元素的前一个下标
counting[nums[i]-min]--;
}
//将结果赋值回原数组
System.arraycopy(result,0,nums,0,nums.length);
return nums;
}
}
基数排序
class Solution {
//基数排序(基数:-9~9)
public int[] sortArray(int[] nums) {
//判空
if(nums==null) return nums;
//最长数字
int max=0;
for(int num:nums){
if(Math.abs(num)>max){
max=Math.abs(num);
}
}
//计算最长数字的长度
int maxDigitLength=0;
while(max!=0){
maxDigitLength++;
max/=10;
}
//通过计数排序进行基数排序
int[] counting=new int[19];//下标 [0, 18] 对应基数 [-9, 9]
int[] result=new int[nums.length];
int dev=1;//用来计算基数
for(int n=0;n<maxDigitLength;n++){//最外面循环使用变量n,内层循环可以用i
//计算基数,计数
for(int element:nums){
int radix=element/dev%10+9;
counting[radix]++;
}
//记录每种元素最后一个的下标
counting[0]--;//计算的为下标=当前个数+之前个数-1
for(int i=1;i<counting.length;i++){
counting[i]+=counting[i-1];
}
//对基数进行排序
//使用倒序遍历的方式完成计数排序!!!,从前往后会报错!!!
for(int i=nums.length-1;i>=0;i--){
//下标调整
int radix=nums[i]/dev%10+9;
result[counting[radix]]=nums[i];
counting[radix]--;
}
//计数排序完成后,将结果拷贝回nums数组
System.arraycopy(result,0,nums,0,nums.length);
//将计数数组重置为 0
Arrays.fill(counting,0);
//将计算基数dev上升一位
dev*=10;
}
return nums;
}
}