包含冒泡排序,选择排序,插入排序,希尔排序,快速排序,归并排序,基数排序,堆排序
声明:下面的代码只是一个最基础的实现,没有经过严格的测试。
/**
* 排序
*/
public class Sort {
public static void main(String[] args) {
/* int[] arr=new int[8000000];
for (int i = 0; i < arr.length; i++) {
arr[i]=(int) (Math.random()*80000000);
}
long start=System.currentTimeMillis();*/
//int[] temp=new int[arr.length];
//mergeSort(arr,0,arr.length-1,temp);
//quickSort(arr,0,arr.length-1);
//shellSort(arr);
/* heapSort(arr);
long end=System.currentTimeMillis();
System.out.println("执行时间:"+(end-start)/1000+"秒");*/
int[] arr={5,7,8,3,1,2,4,6,0,-3,0};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 冒泡排序
* 基本原理:从尾至头循环一遍,相邻的两个元素进行比较,如果前面的元素比后面大,则两个元素进行交换。
* 每进行一次循环,则会将一个“最大”的元素移至尾部,类似水池冒泡,每进行一次循环,一个水泡就会冒上来。
*
* 上面的操作循环length-1次时,所有元素排序完毕。
* 执行时间(本电脑中);8w随机数花费16秒
* 时间复杂度:O(n^2)
* @param arr
*/
static void bubbleSort(int[] arr){
for (int j = 0; j < arr.length-1; j++) {
//循环次数-j的原因:因为每循环一次,就会将一个“最大”的元素移至尾部,这些元素不用再参与比较了。只有前面的元素才参与比较。
//可以不用-j依然可以实现排序,即已经排序好的元素继续参与排序,但效率低。
for (int i = 0; i < arr.length-1-j; i++) {
if(arr[i]>arr[i+1]){
int temp=arr[i+1];
arr[i+1]=arr[i];
arr[i]=temp;
}
}
}
}
/**
* 选择排序,第一次循环寻找最小元素,放到第一个位置上。第二次循环寻找次小元素,放到第二个位置上。以此类推
* 执行时间(本电脑中);8w随机数花费7秒
* 时间复杂度:O(n^2)
* @param arr
*/
static void selectionSort(int[] arr){
//1,第一次循环寻找最小元素,第二次循环寻找次小元素,以此类推,循环。循环次数为length-1次。当循环length-1次时,只剩最后一个元素没排序,
//而该元素肯定是最大的
//j是每次比较时的起始指针,从该起始指针开始往后查找最小值,查找到之后和起始指针对应的值交换。起始指针移向下一位
for (int j = 0; j < arr.length-1; j++) {
//从第j个元素开始比较,每轮比较前都认为j是最小元素对应的下标
int minIndex=j;
for (int i = j+1; i < arr.length; i++) {
//循环获取最小的元素的下标
if(arr[i]<arr[minIndex]){
minIndex=i;
}
}
//找到最小的元素后,和第j个元素交换位置(第一次循环是和第一个元素交换,第二次循环是和第二个元素交换,第三次循环是和第三个元素交换,依次类推)
int temp=arr[minIndex];
arr[minIndex]=arr[j];
arr[j]=temp;
}
}
/**
* 插入排序(逻辑上认为数组分两部分,前面的有序部分和后面的无序部分,循环遍历无序部分,将无序部分的元素插入到有序部分中(需找到合适的插入位置))
* 寻找插入位置的同时需要移动元素
* 特点:序列的有序性越高,则每次移动次数越少,效率越高
* 运行时间(本电脑中);8w随机数花费1秒,80w随机数超过100秒
* 时间复杂度:O(n^2)
* @param arr
*/
static void insertionSort(int[] arr){
//默认认为第一个元素是有序的,从第二个元素开始才是无序的部分
for (int i = 1; i < arr.length; i++) {
//无序部分需要插入的元素(无序部分的第一个元素)
int insertVal=arr[i];
//待插入的位置。初始值为有序部分的最后一个元素的下标
int insertIndex=i-1;
//如果待插入元素比有序部分的元素小(有效部分从右向左遍历,因此insertIndex--),则说明还没有找到待插入的位置,继续寻找。直到大于或者等于为止。
//如果没有找到,说明待插入的位置在前面,每次循环需要移动元素(这种移动方式会导致移动的相邻的两个元素相同(值覆盖),因此最后需要重新赋值)
while(insertIndex>=0 && insertVal<arr[insertIndex]){
//如果待插入的值比当前有序序列的值小,说明待人插的值一定在当前有序序列的值的前面,当前有序序列的值后移一位,给待插入的值让出地方
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//当while循环结束时,说明待插入的位置已经找到
arr[insertIndex + 1]=insertVal;
}
}
/**
* 希尔排序:遵循先小组进行排序,每一组内先有序。然后大组排序,整体上的有序性随着组的增大而提升,当步长为0时,则排好序了
* 采用冒泡法(效率非常低,极少使用。平常说的希尔排序都是指希尔-插入法)
* @param arr
*/
static void shellTestSort(int[] arr){
int length=arr.length;
//gap为每次分组之后的步长(增量)
for (int gap = length/2; gap >0;gap/=2) {
//遍历各组中的所有元素(共有gap组,每个组有length/2个元素),步长为gap。举例当length=10,gap=2时,10个元素分
//2组,每个组两两进行比较,恰好需要8次比较
for(int i=gap;i<length;i++){
//为什么是j-=gap?因为无论是冒泡还是插入算法,需要两层循环。而j-=gap保证了当j>gap时,会开始第二轮循环
for(int j=i-gap;j>=0;j-=gap){
//如果当前元素大于加上步长后的那个元素,则进行交换
if(arr[j]>arr[j+gap]){
int temp=arr[j];
arr[j]=arr[j+gap];
arr[j+gap]=temp;
}
}
}
}
}
/**
* 希尔排序,采用插入法
* 遵循先小组进行排序,每一组内先有序。然后大组排序,整体上的有序性随着组的增大而提升,当步长为0时,则排好序了
* 分组是为了提升序列的有序性,对于插入排序,有序性越高,效率越高。
* 运行时间(本电脑中);80w随机数花费小于1秒,800w随机数4秒
* 时间复杂度:O(n^(1.3-2))
* 参考链接https://blog.csdn.net/qq_39207948/article/details/80006224
* @param arr
*/
static void shellSort(int[] arr){
int length=arr.length;
//gap为每次分组之后的步长(增量)
for (int gap = length/2; gap >0;gap/=2) {
//1,直接对照插入排序,将+1,-1的操作,替换成+gap和-gap的操作即可。因为逻辑分组后,每组序列的相邻元素下标不再相差为1,而是相差gap
//2,为什么i是从gap开始,因为插入排序从1开始,即第二个元素。分组后,第二个元素对应的下标就是gap
//3,i++而不是i+=gap,说明并不是一个组排序结束之后再去对另一个组进行排序,而是各组轮流排序。比如第一组4个元素,第二组4个元素,第一次循环比较
//第一组的前2个元素,和第二组的前2个元素。第二次循环比较第一组的前3个元素和第二组的前3个元素。第三次循环时,第一组和第二组都比较完毕。
for(int i=gap;i<length;i++){
//无序部分需要插入的元素(无序部分的第一个元素)
int insertVal=arr[i];
//待插入的位置。初始值为有序部分的最后一个元素的下标
int insertIndex=i-gap;
while(insertIndex>=0 && insertVal<arr[insertIndex]){
//如果待插入的值比当前有序序列的值小,说明待人插的值一定在当前有序序列的值的前面,当前有序序列的值后移一位,给待插入的值让出地方
arr[insertIndex + gap] = arr[insertIndex];
insertIndex-=gap;
}
//当while循环结束时,说明待插入的位置已经找到
arr[insertIndex + gap]=insertVal;
}
}
}
/**
* 1,快速排序:每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了
* 2,本程序设定“基准数”为待排序列中最左边的数
* 参考链接:https://blog.csdn.net/shujuelin/article/details/82423852
* 运行时间(本电脑中);800w随机数不到2秒
* 时间复杂度:平均,最好,都是O(nlogn),最坏是O(n^2)
* @param arr
* @param start
* @param end
*/
static void quickSort(int[] arr, int start,int end){
//右指针,每次递减
int right=end;
//左指针,每次递增 left 一定不能大于right。当left和right相等时,left或者right下标对应的数和基准数交换位置
int left=start;
//递归退出条件
if(left>right){
return;
}
//基准数,第一次排序时,基准数是数组中的第一个数
int pivot=arr[start];
while (left<right){
//右指针从右向左查找比基准数小的数(如果以最左边的数为基准数,则一定是右指针先查找)
while(pivot<=arr[right] && left<right){
right--;
}
//左指针从左向右查找比基准数大的数
while(pivot>=arr[left] && left<right){
left++;
}
//代码执行到这里时,右指针和左指针一定找到了一个比基准数小和比基准数大的数,交换位置
int temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
}
//大循环结束时(left<right不满足),left一定等于right,左指针和右指针相遇,这时相遇位置对应的元素和基准数交换位置
int temp=arr[left];
arr[left]=pivot;
arr[start]=temp;
/* 至此,第一排序结束。此时数组被分成两部分,一部分是基准数右边都比基准数大的数,一部分是基准数左边比基准数都小的数。
对基准数两边的序列重复上边的步骤,使用递归*/
//右半部分进行递归排序
quickSort(arr,right+1,end);
//左半部分进行递归排序
quickSort(arr,start,right-1);
}
/**
* 归并排序
* 采用分治法。通过递归的方式,将数组分成两部分,然后每一部分继续二分,直到最小的序列只有两个元素(start<end)。然后合并。借助
* 一个临时数组,存放两个合并的序列数据,序列合并完毕后,将临时数组的中的元素再复制到这两个序列构成的长序列中。
* 运行时间(本电脑中);800w随机数2秒
* 时间复杂度:平均,最好,最差都是O(nlogn)
* @param arr
* @param start 拆分(逻辑上)序列的起始下标
* @param end 拆分(逻辑上)序列的结束下标
* @param temp
*/
static void mergeSort(int[] arr, int start,int end,int[] temp){
if(start<end){
int middle=(start+end)/2;
mergeSort(arr, start, middle, temp);
mergeSort(arr, middle+1, end, temp);
mergeArray(arr,start,middle,end,temp);
}
}
/**
* 参考链接:https://www.runoob.com/w3cnote/sort-algorithm-summary.html
* 合并 :将两个序列arr[start--middle],arr[middle+1-end]合并(实际上是一个数组,根据下标在逻辑上分成两个数组)
* @param arr
* @param start 左半部分序列起点
* @param middle middle+1为右半部分序列的起点,middle为左半部分序列最后一个元素的下标
* @param end 右半部分序列最后一个元素的下标
* @param temp
*/
static void mergeArray(int[] arr, int start,int middle,int end,int[] temp){
//定义两个指针,都从序列的最左边开始
int startP = start,middleP=middle+1,tPointer=0;
//循环条件,左半部分序列的指针小于middle且右半部分的指针小于end
while(startP<=middle && middleP<=end){
//如果左指针对应的节点小于等于右指针对应的元素,则将左指针对应的元素放到临时数组中。反之亦然
if(arr[startP]<=arr[middleP]){
temp[tPointer]=arr[startP];
tPointer ++;
startP ++ ;
}else{
temp[tPointer]=arr[middleP];
tPointer ++;
middleP ++;
}
}
//如果左指针还没移动到头(左半部分的头对应的下标是middle),则将剩下的那部分移动到临时数组
while(startP<=middle){
temp[tPointer]=arr[startP];
tPointer ++;
startP++;
}
//同上
while(middleP<=end){
temp[tPointer]=arr[middleP];
tPointer ++;
middleP++;
}
//临时数组中的值复制回原数组
for(int i=0;i<tPointer;i++){
arr[start + i] = temp[i];
}
}
/**
* 补充内容:合并两个有序的数组,归并排序中就是不断合并两个有序的数组
* @param a 有序数组a
* @param aLength 数组a长度
* @param b 有序数组b
* @param bLength 数组b长度
*/
static void merge2Array(int[] a, int aLength,int[] b,int bLength){
int[] temp=new int[aLength+bLength];
//a数组和b数组分别定义两个指针,都从最左边开始
int aPointer = 0,bPointer=0,tPointer=0;
while(aPointer<aLength && bPointer<bLength){
if(a[aPointer]<b[bPointer]){
temp[tPointer]=a[aPointer];
tPointer++;
aPointer++;
}else{
temp[tPointer]=b[bPointer];
tPointer++;
bPointer++;
}
}
//如果b数组的指针还没移动到头,则a数组的所有元素肯定已经放到temp数组了,将b数组剩下的元素按顺序放入temp数组
while(bPointer<bLength){
temp[tPointer]=b[bPointer];
tPointer++;
bPointer++;
}
//a数组的指针还没移动到头,同上
while(aPointer<aLength){
temp[tPointer]=a[aPointer];
tPointer++;
aPointer++;
}
System.err.println(Arrays.toString(temp));
}
/**
* 基数排序(先不考虑负数和小数的情况),
* 用空间换时间,是将整数按位数切割成不同的数字,然后按每个位数分别比较。每比较一轮将桶中的数据按顺序赋值给arr。比较的次数取决于最大数的位数(不考虑负数和小数)
* 运行时间(本电脑中);800w随机数小于1秒,8000w个随机数排序报OOM,
* 时间复杂度:平均,最好,最差都是O(n * k,k表示桶的个数)
* @param arr
*/
static void radixSort(int[] arr){
//1,先获取数组中最大数的位数(长度)
int max=arr[0];
for (int i = 1; i < arr.length; i++) {
if(arr[i]>max){
max=arr[i];
}
}
int maxLength=(max+"").length();
//定义10个桶,表示0-9。是一个二维数组。
int[][] bucket=new int[10][arr.length];
for(int i=0,n=1;i<maxLength;i++,n*=10){
//每个桶有多少个元素,即二维数组每行有多少个有效数据
int[] bucketSize=new int[10];
/* 取出数组中的每个元素,按照对应位数的值,放到对应的桶中。第一次取的个位上的值,公式是:n/1 % 10。取十位上的值对应的
公式为n/10 % 10,百位为n/100 % 10 */
for (int j = 0; j < arr.length; j++) {
//例如,如果取个位上的值,某个元素个位上的值为3,则将其放入下标为3的桶中,桶的容量+1
int digit=arr[j]/n % 10;
bucket[digit][bucketSize[digit]]=arr[j];
//桶容量自增
bucketSize[digit]++;
}
//放入到桶中后,将桶中的元素按顺序取出,放入到原数组arr中
int index=0;
for(int k=0;k<bucketSize.length;k++){
//获取每一个桶的容量
int size=bucketSize[k];
for(int s=0;s<size;s++){
//桶中的元素取出,赋值给arr
arr[index]=bucket[k][s];
index++;
}
}
}
}
/**
* 堆排序
* 堆的定义:堆是一个近似完全二叉树的结构。如果是大顶堆,则每一个父节点都大于等于其子节点。小顶堆反之。对应元素对应
* 数组中的第一个元素
* 运行时间(本电脑中);800w随机数小于4秒.
* 时间复杂度:平均,最好,最差都是O(nlogn)
* 参考链接:https://www.cnblogs.com/luomeng/p/10618709.html
* @param arr 需要排序的数组
*/
static void heapSort(int arr[]){
//将无序队列构成一个标准的堆,升序是大顶堆,降序是小顶堆。这里是成一个大顶堆
// arr.length/2-1(二叉树顺序存储中的公式)为第一个非叶子节点的下标,该节点调整后,再调整上一个节点,直到序列中的第一节点
//序列[4,2,5,1,6,3,0,8]执行完该代码成一个大顶堆:[8, 6, 5, 2, 4, 3, 0, 1]
for(int i = arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
//将堆顶元素与末尾元素交换,最大元素“沉”到数组末端。“沉”到末端的元素,不再参与排序和堆结构调整,所以j--,堆会越来越小
for(int j = arr.length-1; j>=0;j--){
int temp=arr[j];
arr[j]=arr[0];
arr[0]=temp;
//重新调整结构使其满足堆定义,然后继续交换堆顶元素与末尾元素,反复执行,直到序列有序
//为什么第二个参数是0?因为堆顶元素与最后一个元素交换之后,只有下标为0的元素不满足堆的定义,其他的元素仍然
//满足堆的定义,因此只调整下标为0的元素顺序即可
adjustHeap(arr,0,j);
}
}
static void adjustHeap(int[] arr,int i,int length){
//先取出当前元素的值,保存到临时变量中
int temp=arr[i];
// k=i*2+1 ,其中k是i的左子节点 ,顺序存储二叉树中的公式,i*2+2表示i的右子节点
for(int k=i*2+1;k<length;k=k*2+1){
//如果右子节点的值比左子节点大,k指向右子节点
if(k+1<length && arr[k]<arr[k+1]){
k++;
}
//如果子节点(左节点或者右节点)大于父节点,则将该子节点的值赋给父节点
if(arr[k] > temp){
arr[i] = arr[k];
// i指向k,继续循环
i = k;
}else{
break;
}
}
//当for循环结束后,已经将以i为父节点的数的最大值,放到了最顶(局部),
//将之前temp的值(进入循环之前的以i为下标的值)赋给调整后的位置上
arr[i]=temp;
}
}