攻克十大经典排序算法【代码详细注释】

前言

最近在学习算法知识,算法是必须要掌握的东西,而排序算法是最经典的算法知识,重要性就不必多说啦~
还在学习中,对学到的知识进行简单的记录,如果有什么问题欢迎大佬指正。
在这里插入图片描述

一、冒泡排序

冒泡排序是最出名的算法之一,从序列的一端开始往另一端冒泡,依次比较相邻两个元素的大小。

//冒泡排序,从小到大
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;
	}
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值