前言
最近在学习算法知识,算法是必须要掌握的东西,而排序算法是最经典的算法知识,重要性就不必多说啦~
还在学习中,对学到的知识进行简单的记录,如果有什么问题欢迎大佬指正。
一、冒泡排序
冒泡排序是最出名的算法之一,从序列的一端开始往另一端冒泡,依次比较相邻两个元素的大小。
//冒泡排序,从小到大
public void bubblesort(int[] arr) {
//外层for循环表示比较的轮次
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]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
最外层for循环每经过一轮,剩余数字中的最大值就会被移动到当前轮次的最后一位。但是,如果最外层for循环还没有执行到最后一轮但数组已经有序了,这时代码还在依次比较相邻的元素。
对刚刚代码进行优化:
//对第一种冒泡排序的代码进行优化
public void bubblesort(int[] arr) {
boolean swaped = true;//记录当前轮次是否发生交换
for(int i=0;i<arr.length-1;i++) {
//swaped为false,则上一轮没有发生交换,数组已经有序
if(!swaped) return;
//将swaped设置为false,若当前轮发生交换,则设置为true
swaped = false;
for(int j=0;j<arr.length-i-1;j++) {
if(arr[j]>arr[j+1]) {
//交换元素
if(arr[j]>arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swaped = true;//数据发生了交换,说明当前数组还没有排序完成
}
}
}
}
}
设置一个布尔类型的变量,用来表示当前轮次是否发生了交换,如果一轮比较中没有发生交换,说明此时数组已经有序,则立即停止排序。
二、选择排序
选择排序的思想:双重循环遍历数组,找到每一轮中最小元素的下标,将其交换至首位。
//选择排序,从小到大
public void selectionsort(int[] arr) {
int min;//记录每一轮中最小值的下标
for(int i=0;i<arr.length-1;i++) {
min = i;
//寻找最小值的下标
for(int j=i+1;j<arr.length;j++) {
if(arr[j]<arr[min]) {
min = j;
}
}
//将最小值和当前轮的首位进行交换
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
冒泡排序和选择排序的不同点:冒泡排序在比较的过程中不断发生交换,而选择排序是记录最小值下标,遍历完成后才进行一次交换,减少了交换次数。
二元选择排序:想一下,每遍历一轮,我们找出了当前轮次的最小值,为什么一起找出最大值呢?这就是二元选择排序,可以将循环遍历的范围减小一半。
//二元选择排序
public void selectionsort(int[] arr) {
int min;//记录最小值的下标
int max;//记录最大值的下标
//注意:每一轮都将当前轮的最大值和最小值放到了相应位置,减少了一半的遍历范围
for(int i=0;i<arr.length/2;i++) {
min = i;
max = i;
//第i轮已经确定了数组的i个最大值,所以在求最大值和最小值时,范围也缩小了
for(int j=i+1;j<arr.length-i;j++){
if(arr[j]>arr[max]) {
max = j;
}
if(arr[j]<arr[min]) {
min = j;
}
}
//如果min=max,说明此时数组已经有序
if(min == max) return;
//交换最小值和首位
swap(arr,i,min);
//如果最大值下标在首位,但是上一过程中我们交换了首位和最小值,所以更新最大值的下标
if(max==i) {
max = min;
}
swap(arr,max,arr.length-i-1);
}
}
//交换两个元素
public void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
三、插入排序
插入排序的两种思想:
交换法:在数字插入的过程,不断的和前面的数字交换,直到找到合适的位置
移动法:在数字插入的过程,与前面的数组不断的进行比较,前面的数字不断的向后挪出位置,当新数字找到合适位置后,执行一次插入即可。
交换插入排序:
//交换插入排序,从小到大
public void insertsort(int[] arr) {
//当数组中只有一个元素时,不需要插入
for(int i=1;i<arr.length;i++) {
int index = i;//记录插入元素的下标
while(index>=1 && arr[index-1]>arr[index]) {
swap(arr,index-1,index);
index--;//交换后,需插入的元素下标更新
}
}
}
//交换两个元素
public void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
移动插入排序:
//移动插入排序,从小到大
public void insertsort(int[] arr) {
//从第二个数字开始插入
for(int i=1;i<arr.length;i++) {
int index = i-1;//记录前一个元素的下标
int num = arr[i];//需插入的元素
while(index>=0 && arr[index]>num) {
//向后挪出位置
arr[index+1] = arr[index];
index--;
}
//找到合适位置,插入元素
arr[index+1] = num;
}
System.out.println(Arrays.toString(arr));
}
四、希尔排序
希尔排序是对插入排序的优化,插入排序每次交换相邻元素,而希尔排序每次排序时可以交换不相邻的元素。
希尔排序的思想:
- 将待排序的数组按照间隔分为多个子数组(注意是跳跃间隔取值,不是连续的一段数组),分别进行插入排序
- 逐渐缩小间隔进行下一轮排序
- 当间隔小于1时停止排序。
增量:每一遍排序的间隔称为增量,所有的增量组成的序列称为增量序列。
//希尔排序,从小到大
public void shellsort(int[] arr) {
//确定增量
for(int gap=arr.length/2;gap>0;gap/=2) {
//groupstart同一间隔序列的第一个元素的下标
for(int groupstart=0;groupstart<gap;groupstart++){
//从同一间隔序列的第二个元素开始插入排序
for(int current=groupstart+gap;current<arr.length;current+=gap) {
int preindex = current-gap;//同一间隔序列中前一个元素的下标
int cur = arr[current];//需插入的元素
while(preindex>=0 && arr[preindex]>cur) {
arr[preindex+gap] = arr[preindex];
preindex-=gap;
}
arr[preindex+gap] = cur;
}
}
}
}
分析上面代码,可以看出,我们是处理完一组间隔序列后,再处理下一组间隔序列,这很符合我们的思维,但是对计算机来说,这样处理是在不同间隔之间不断跳跃的,所以我们可以让计算机来访问一段连续的数组,以增量为步长完成插入排序。
//希尔排序,从小到大
public void shellsort(int[] arr) {
//确定增量
for(int gap=arr.length/2;gap>0;gap/=2) {
for(int i=gap;i<arr.length;i++) {
int preindex = i-gap;//同一间隔序列的前一个元素
int current = arr[i];//需插入的元素
while(preindex>=0 && arr[preindex]>current) {
arr[preindex+gap] = arr[preindex];
preindex-=gap;
}
//将需插入的元素放到合适的位置
arr[preindex+gap]=current;
}
}
}
五、堆排序
什么是堆?
符合以下两个条件之一的完全二叉树称为堆:
- 根节点的值>=子节点的值称为大顶堆;
- 根节点的值<=子节点的值称为小顶堆。
堆排序的思想:
- 构建初始大顶堆,取出堆顶元素;
- 将剩余数字调整成大顶堆,再取出堆顶元素;
- 循环往复,完成整个排序。
在完成代码之前,我们先了解一下完全二叉树的性质(将根节点的下标视为0):
- 对于完全二叉树的第i个数,左子节点的下标为left=2i+1;
- 对于完全二叉树的第i个数,右子节点的下标为right=left+1;
- 对于有n个元素的完全二叉树,最后一个非叶子节点的下标为:n/2-1;
//堆排序,从小到大
public void heapsort(int[] arr) {
//构建初始大顶堆
buildMaxHeap(arr);
for(int i=1;i<arr.length;i++) {
//交换元素,将根节点与最后一个节点交换
swap(arr,0,arr.length-i);
//调整剩余数组成为大顶堆
maxHeapify(arr,0,arr.length-i);
}
}
//构建初始大顶堆
public void buildMaxHeap(int[] arr) {
//从最后一个非叶子节点开始调整大顶堆
for(int i=arr.length/2-1;i>=0;i--) {
maxHeapify(arr,i,arr.length);
}
}
//调整大顶堆,count为当前二叉树剩余元素个数
public void maxHeapify(int[] arr,int root,int count) {
//左右子节点的下标
int left = 2*root+1;
int right = left+1;
int max = root;//记录根节点与左右子节点的最大值下标
if(left<count && arr[left]>arr[max]) {
max = left;
}
if(right<count && arr[right]>arr[max]) {
max = right;
}
//如果max!=root,则需要调整大顶堆,交换元素
if(max != root) {
swap(arr,max,root);
maxHeapify(arr,max,count);
}
}
//交换两个元素
public void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
六、快速排序
快速排序的思想:
- 在数组中取一个数,作为基数;
- 遍历数组,把比基数大的放在右边,比基数小的放在左边;遍历完成后,数组被分成左右两个区域;
- 将左右两个区域视为两个数组,重复前面的步骤,直到排序完成。
简单分区算法:
//快速排序,从小到大
public void quicksort(int[] arr) {
quickSort(arr,0,arr.length-1);
}
public void quickSort(int[] arr,int start,int end) {
//当分区中没有元素或只有一个元素时退出递归
if(start>=end) return;
int mid = partition(arr,start,end);//基数下标
quickSort(arr,start,mid-1);//对左边区域快速排序
quickSort(arr,mid+1,end);//对右边区域快速排序
}
//计算基数下标
public int partition(int[] arr,int start,int end) {
//以第一个元素为基数
int pivot = arr[start];
//分区
int left = start+1;
int right = end;
while(left<right) {
//寻找比基数大的元素
while(left<right && arr[left]<pivot) left++;
//交换两个数,使比基数小的在左区域,比基数大的在右区域
if(left!=right) {
swap(arr,left,right);
right--;
}
}
//当left==right时,单独比较arr[right]和pivot
if(left==right && arr[right]>pivot) right--;
//将基数和中间数进行交换
if(right!=start) {
swap(arr,right,start);
}
//返回中间值下标
return right;
}
//交换两个元素
public void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
if(left==right && arr[right]>pivot) right- -; 这行代码处理了三种情况:
- 当剩余数组中只有最后一个元素大于基数时
- 当[left,right]区间只有一个元素时
- 当剩余数组中的元素都大于基数时,right会一直减小,直到和left相等退出循环,此时还没有比较left所有位置的值和基数。
双指针分区算法:
从left开始,遇到比基数大的数,记录其下标;从right开始,遇到比基数小的数,记录其下标;然后交换这两个数,其余和简单分区算法一样。
//双指针分区算法
public void quicksort(int[] arr) {
//对数组进行快速排序
quickSort(arr,0,arr.length-1);
}
//快速排序
public void quickSort(int[] arr,int start,int end) {
//当区域元素小于2个时退出递归
if(start>=end) return;
int mid = partition(arr,start,end);//计算基数下标
quickSort(arr,start,mid-1);//对左边区域快速排序
quickSort(arr,mid+1,end);//对右边区域快速排序
}
//计算基数下标
public int partition(int[] arr,int start,int end) {
//将第一个元素作为基数
int pivot = arr[start];
//分区
int left = start+1;
int right = end;
while(left<right) {
//从left开始,寻找第一个大于基数的下标
while(left<right && arr[left]<=pivot) left++;
//从right开始,寻找第一个小于基数的下标
while(left<right && arr[right]>=pivot) right--;
//交换这两个数
if(left!=right) {
swap(arr,left,right);
left++;
right--;
}
}
//当left==right时,right左边都比基数小,right右边都比基数大,只需比较arr[right]和pivot
if(left==right && arr[right]>pivot) right--;
if(right!=start) {
swap(arr,start,right);
}
return right;
}
//交换两个元素
public void swap(int[] arr,int i,int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
七、归并排序
归并排序的思想:
将1个数字组成的有序数合并成一个包括2个数的有序数组,再将2个数字的有序数组合并成包括4个数的有序数组…直到整个数组排序完成。
//归并排序
public void mergesort(int[] arr) {
if(arr.length==0) return;
//将结果保存在一个新数组中
int[] res = mergeSort(arr,0,arr.length-1);
//将结果拷贝到arr数组
for(int i=0;i<res.length;i++) {
arr[i] = res[i];
}
}
//分区
public int[] mergeSort(int[] arr,int start,int end) {
//只剩下一个数字时停止拆分,返回单个数字组成的数组
if(start==end) return new int[] {arr[start]};
int mid = (end-start)/2+start;//防止溢出
int[] left = mergeSort(arr,start,mid);//对左区域进行分区
int[] right = mergeSort(arr,mid+1,end);//对右区域进行分区
//合并左右区域
return merge(left,right);
}
//合并两个有序数组
public int[] merge(int[] arr1,int[] arr2) {
int[] res = new int[arr1.length+arr2.length];
//双指针,分别指向两个数组的元素
int left = 0;
int right = 0;
int index = 0;
//合并两个有序数组
while(left<arr1.length && right<arr2.length) {
if(arr1[left]<=arr2[right]) {
res[index++] = arr1[left++];
} else {
res[index++] = arr2[right++];
}
}
while(left<arr1.length) {
res[index++] = arr1[left++];
}
while(right<arr2.length) {
res[index++] = arr2[right++];
}
return res;
}
为了减少在递归过程中不断开辟新空间的问题,可以在归并排序之前先开辟一个临时空间,在递归过程中统一使用此空间进行归并。
//归并排序
public void mergesort(int[] arr) {
if(arr.length==0) return;
//先开辟一个临时空间
int[] res = new int[arr.length];
mergeSort(arr,0,arr.length-1,res);
}
//对arr的[start,end]区间归并排序
public void mergeSort(int[] arr,int start,int end,int[] res) {
//只剩一个数组,停止拆分
if(start==end) return;
int mid = (end-start)/2+start;
//拆分左区域,将归并排序的结果保存到res数组的[start,mid]区间
mergeSort(arr,start,mid,res);
//拆分右区域,将归并排序的结果保存到res数组的[mid+1,end]区间
mergeSort(arr,mid+1,end,res);
//合并左右区域
merge(arr,start,end,res);
}
public void merge(int[] arr,int start,int end,int[] res) {
int mid = (end-start)/2+start;
//确定左右分区的首尾位置
int s1 = start;
int end1 = mid;
int s2 = mid+1;
int end2 = end;
int index=start;
while(s1<=end1 && s2<=end2) {
if(arr[s1]<=arr[s2]) {
res[index++] = arr[s1++];
} else {
res[index++] = arr[s2++];
}
}
//将剩余数字补到结果数组之后
while(s1<=end1) {
res[index++] = arr[s1++];
}
while(s2<=end2) {
res[index++] = arr[s2++];
}
//将[start,end]区间的数字拷贝给arr数组
for(int i=start;i<=end;i++) {
arr[i] = res[i];
}
}
八、计数排序
计数排序的思想:
- 根据待排序数组的范围计算计数数组的长度
- 统计数字出现的个数,使计数数组的下标与待排序数组的元素值对应起来
- 根据个数计算每个元素在排序完成后的位置
- 将元素赋值到对应位置
//计数排序
public void countingsort(int[] arr) {
if(arr.length==0) return;
//计算待排序数组的范围
int min = arr[0];
int max = arr[0];
for(int i=0;i<arr.length;i++) {
if(arr[i]>max) {
max = arr[i];
} else if(arr[i]<min) {
min = arr[i];
}
}
//计数数组的长度
int len = max-min+1;
int[] count = new int[len];
//统计待排序数组中每个元素的个数
for(int i=0;i<arr.length;i++) {
count[arr[i]-min]++;
}
//计算相等元素的最后一个下标位置,相等元素的最后一个下标位置=前面对自己小的数字的总和+自己的数量-1
//将下标位置也保存在count中,更新count数组
count[0]--;
for(int i=1;i<count.length;i++) {
count[i]+=count[i-1];
}
int[] res = new int[arr.length];//保存排序的结果
for(int i=arr.length-1;i>=0;i--) {
//获取此元素在结果数组中的下标
int index = count[arr[i]-min];
res[index] = arr[i];
count[arr[i]-min]--;//更新,指向此元素的前一个下标
}
//将结果赋值给arr数组
for(int i=0;i<res.length;i++) {
arr[i] = res[i];
}
}
九、基数排序
基数排序的思想:
- 找出数组中的最大值,计算最大值的位数max
- 获取数组中每个数字的基数
- 遍历max轮数组,每轮按照基数对其进行计数排序
对不含负数的数组进行基数排序
//基数排序
public void radixsort(int[] arr) {
if(arr.length==0) return;
//计算排序数组中的最大值
int max = arr[0];
for(int i=0;i<arr.length;i++) {
if(arr[i]>max) {
max = arr[i];
}
}
//计算最大值的位数
int len = 0;
while(max!=0) {
len++;
max/=10;
}
//使用计数排序对基数进行排序
int[] count = new int[10];
int dev = 1;
for(int i=0;i<len;i++) {
//统计个数
for(int value:arr) {
int val = value/dev%10;//基数
count[val]++;
}
//计算初始位置
for(int j=1;j<count.length;j++) {
count[j]+=count[j-1];
}
int[] res = new int[arr.length];
//使用倒序遍历完成计数排序
for(int j=arr.length-1;j>=0;j--) {
int radix = arr[j]/dev%10;
count[radix]--;
res[count[radix]]=arr[j];
}
//计数排序后,将结果拷贝回arr数组
System.arraycopy(res, 0, arr, 0, arr.length);
dev*=10;
//将计数数组重置为0
Arrays.fill(count, 0);
}
}
对含负数的数组进行基数排序
在对基数进行计数排序的时候,申请长度为19的计数数组,用来存储[-9,9]之间的所有整数。
//基数排序
public void radixsort(int[] arr) {
if(arr.length==0) return;
//计算排序数组中的绝对值的最大值
int max = arr[0];
for(int i=0;i<arr.length;i++) {
if(Math.abs(arr[i])>max) {
max = Math.abs(arr[i]);
}
}
//计算最大值的位数
int len = 0;
while(max!=0) {
len++;
max/=10;
}
//使用计数排序对基数进行排序
int[] count = new int[19];//[-9,9]
int dev = 1;
for(int i=0;i<len;i++) {
//统计个数
for(int value:arr) {
int val = value/dev%10+9;//基数
count[val]++;
}
//计算初始位置
for(int j=1;j<count.length;j++) {
count[j]+=count[j-1];
}
int[] res = new int[arr.length];
//使用倒序遍历完成计数排序
for(int j=arr.length-1;j>=0;j--) {
int radix = arr[j]/dev%10+9;
count[radix]--;
res[count[radix]]=arr[j];
}
//计数排序后,将结果拷贝回arr数组
System.arraycopy(res, 0, arr, 0, arr.length);
dev*=10;
//将计数数组重置为0
Arrays.fill(count, 0);
}
}
十、桶排序
桶排序的思想:
- 将区间划分为n个相同大小的子区间,每个区间称为一个桶;
- 遍历数组,将每个元素装入桶中;
- 对每个桶中的元素进行排序,采用其他排序算法;
- 最后按照顺序将所有桶的元素合并起来。
装桶时用链表,桶内排序用数组:
//桶排序
public void bucketsort(int[] arr) {
if(arr.length==0) return;
//计算最大值最小值
int max = arr[0];
int min = arr[0];
for(int i=0;i<arr.length;i++) {
if(arr[i]>max) {
max = arr[i];
} else if(arr[i]<min) {
min = arr[i];
}
}
//确定取值范围
int range = max-min;
int bucketcount = 100;//设置桶的个数,可以随意修改
//桶区间
double gap = range*1.0/(bucketcount-1);
//桶序号,桶中元素
Map<Integer,Deque<Integer>> buckets = new HashMap<>();
for(int value:arr) {
//计算value属于哪个桶
int index = (int) ((value-min)/gap);
//判断这个桶中是否有数据
if(!buckets.containsKey(index)) {
buckets.put(index,new LinkedList<>());
}
//向桶中放入数据
buckets.get(index).add(value);
}
int index = 0;
//对每个桶进行排序
for(int i=0;i<bucketcount;i++){
//获取桶中数据
Deque<Integer> bucket = buckets.get(i);
if(bucket==null) continue;
// 将链表转换为数组
int[] arrInBucket = bucket.stream().mapToInt(Integer::intValue).toArray();
//对数组进行插入排序
insertsort(arrInBucket);
//将排序结果放入桶内
System.arraycopy(arrInBucket, 0, arr, index, arrInBucket.length);
index+=arrInBucket.length;
}
}
//移动插入排序
public void insertsort(int[] arr) {
//从第二个数字开始插入
for(int i=1;i<arr.length;i++) {
int index = i-1;//记录前一个元素的下标
int num = arr[i];//需插入的元素
while(index>=0 && arr[index]>num) {
//向后挪出位置
arr[index+1] = arr[index];
index--;
}
//找到合适位置,插入元素
arr[index+1] = num;
}
}