冒泡排序
每次循环让最大的元素冒泡到最后面
核心规则有四点:
- 指向数组相邻两个元素,比较他们的大小
- 前者比后者大则交换他们的位置
- 后者大,则不交换
- 依次后移每次循环将最大的元素向后移动
如图依次类推,直到最大的元素到最后一位,开始第二次遍历
// 冒泡排序
public static void Sort(int[] array) {
// 1. 每次循环,都能冒泡出剩余元素中最大的元素,因此需要循环 array.length 次
for (int i = 0; i < array.length; i++) {
// 2. 每次遍历,只需要遍历 0 到 array.length - i - 1中元素,因为之后的元素都已经是最大的了
for (int j = 0; j < array.length - i - 1; j++) {
//3. 交换元素
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
}
选择排序
每次在剩余数组中选择一个最大的或者最小的放在数组一侧
核心规则有四点:
- 定义两个变量一个存储当前最大值,另一个存储当前最大值的索引值
- 依次比较后面的元素,如果比当前最大值大,则更新最大值和索引值
- 直到遍历结束,将最大值移到最右端
- 重复上述操作
之后以此类推
// 选择排序
public static int[] sort(int[] array) {
if (array.length == 0) {
return 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; /*将最小数的索引保存*/
}
}
System.out.println("最小数为:"+array[minIndex]);
/*交换最小数和i当前所指的元素*/
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
插入排序
每次抽离一个元素作为临时元素,依次比较和移动之后的元素最终插入正确的位置
核心规则:
- 第一轮抽离第二个元素作为临时元素
- 用临时元素与数组已经排序的元素(假设前面的元素是排好的)对比,如果前面的元素大于,则当前元素向右移动,排序好的索引向前移动
- while循环结束时,说明已经找到了当前待排序数据的合适位置,插入
- 重复上述操作
// 插入排序
public static void insertSort(int[] array) {
if (array.length == 0) {
return array;
}
//当前待排序数据,该元素之前的元素均已被排序过
int currentValue;
for(int i = 0;i<array.length;i++){
//已被排序数据的索引
int preIndex = i;
currentValue = array[preIndex + 1];
//在已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,将比较的元素元素后移一位
while (preIndex >= 0 && currentValue < array[preIndex]) {
//将当前元素后移一位
array[preIndex + 1] = array[preIndex];
//从后向前扫描,继续往前
preIndex--;
}
/*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
array[preIndex + 1] = currentValue;
}
}
希尔排序
一种基于插入排序的快速排序算法,改进了插入排序
因为简单插入排序对于大规模乱序数组很慢,因为元素只能一点一点地从数组的一端移动到另一端。而希尔排序是把待排序的数组按一定数量的分组,对每组使用直接插入排序算法排序,然后缩小数量继续分组排序,随着数量逐渐减少,每组包含的元素越来越多,当数量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的数量,就构成了一个增量序列
选择增量的计算方式为: gap=数组长度/2,缩小增量继续以gap/2的方式形成增量序列.
降序实例
- 第一个增量为7,则原始数组被分为7组,也就是下标之差为7,这7组分别插入排序
- 第二个增量为3,对第一次排序后的数组进行插入排序
- 第三个增量为1,第二次排序后的数组被分为一组,进行插入排序
代码实现
package sort;
/**
* @author peiqi
* @Description: 希尔排序
* @date 2021/10/1015:18
*/
public class ShellSort {
public static int[] sort(int[] array){
if (array.length==0){
return array;
}
int len = array.length;
//增量
int gap = len/2;
//组内待排序数据
int currentValue;
while (gap>0){
//在循环中进行一次插入排序,从增量序列开始
for (int i = gap; i <len ; i++) {
currentValue = array[i];
int preIndex = i-gap;
while (preIndex>=0&&array[preIndex]<currentValue){
//后移距离为增量大小
array[preIndex+gap] = array[preIndex];
preIndex-=gap;
}
array[preIndex+gap] = currentValue;
}
gap = gap/2;
}
return array;
}
}
归并排序
归并排序是建立在归并操作上一种分治法排序算法,将已有序的子序列合并,得到完全的有序序列,
- 先用递归与分治技术将数据序列划分成为越来越小的半子表
- 对半子表排序
- 再用递归方法将排好序的半子表合并成为越来越大的有序序列
降序实例
代码实现
package sort;
import java.util.Arrays;
/**
* @author peiqi
* @Description:
* @date 2021/10/1015:45
*/
public class MergeSort {
public static int[] sort(int[] array){
if (array.length<2){
return array;
}
//将数组拆分
int mid = array.length/2;
//将左右子数组进行拷贝
int []left = Arrays.copyOfRange(array,0,mid);
int []right = Arrays.copyOfRange(array,mid,array.length);
//将左右数组进行分别排序后进行合并
return merge(sort(left),sort(right));
}
public static int[] merge(int[] left,int[]right){
int[] result = new int[left.length+right.length];
for (int index = 0,leftIndex = 0,rightIndex = 0; index <result.length ; index++) {
if (leftIndex>=left.length){
//左边数组元素取完了取右边数组元素
result[index] = right[rightIndex++];
}else if(rightIndex>=right.length){
result[index] = left[leftIndex++];
}else if(left[leftIndex]>right[rightIndex]){
//将大的元素放在数组前面
result[index] = left[leftIndex++];
}else{
result[index] = right[rightIndex++];
}
}
return result;
}
}
快速排序
快速排序是对冒泡排序的一种改进,也是分治法的一个典型的应用
- 首先任取一个数据(比如数组第一个数)作为关键数据,称为基准数
- 然后将比他小的数都放到他前面,比他大的数都放在后面,这个称为分区操作
- 再按此方法对这两部分数据分别进行快速排序,以此达到整个数据变成有序序列
快速排序升序实例
选择48作为基准数,同时引入一个分割指示器(为了快速排序在原数组上操作),这个分割指示器初始化值是数组头元素下标减一,这里是-1,同时交换基准数和数组尾部元素
进行数组的遍历,将数组中的元素与基准数进行比较,为了满足所有比基准数小的数都放到基准数前面,所有比基准数大的数都放到基准数后面,核心规则:
- 当前元素小于等于基准数时,分割指示器右移一位
- 当前元素大于等于基准数,分割指示器保持不变 ,元素也无需交换
- 当前元素下标小于等于分割指示器时当前元素保持不动
- 当前元素下标大于分割指示器时,当前元素和分割指示器所指元素交换
数组遍历,其中元素的变动情况:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
完成了一趟快速排序,再将左右两边继续快速排序
代码实现
package sort;
/**
* @author peiqi
* @Description:
* @date 2021/10/1016:55
*/
public class QuickSort {
public static int[] sort(int[] array,int start,int end){
if (array.length<1||start<0||end>=array.length||start>end){
return null;
}
//分割指示器
int zoneIndex = partition(array,start,end);
if (zoneIndex>start){
sort(array,start,zoneIndex-1);
}
if (zoneIndex<end){
sort(array,zoneIndex+1,end);
}
return array;
}
private static int partition(int[] array, int start, int end) {
//创建基准数
int pivot = (int)(start+Math.random()*(end-start+1));
//定义指示器为头部下标减一
int zoneIndex = start-1;
//基准数与末尾交换位置
swap(array,pivot,end);
for (int i = start; i <=end ; i++) {
//小于基准数
if (array[i]<array[end]){
//向右移动一位
zoneIndex++;
if (i>zoneIndex){
//下标大于指示器,交换
swap(array,i,zoneIndex);
}
}
}
return zoneIndex;
}
public static void swap(int[] array,int i,int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
快速排序的基准数
基准的选取:最优的情况是基准值刚好取在无序区的中间,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数,但是一般很难做到最优。基准的选取一般有三种方式,选取数组的第一个元素,选取数组的最后一个元素,以及选取第一个、最后一个以及中间的元素的中位数(如4 5 6 7, 第一个4, 最后一个7, 中间的为5, 这三个数的中位数为5, 所以选择5作为基准)。
堆排序
许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,我们每次只需要操作数据中的最大元素(最小元素),那么有一种基于二叉堆的数据结构可以提供支持。
二叉堆
他是一个完全二叉树的结构,同时满足对的性质:子结点的键值或者索引总是小于(或者大于)它的父结点,根节点总是最大(或者最小)节点
这就是一个二叉堆,堆排序就是抓住了这一特点,每次都取得堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆
完全二叉树和满二叉树
满二叉树: 除了最后一层无任何节点外,每一层上所有的节点都有2个子结点二叉树
完全二叉树:
是由满二叉树而引出来的,如果我们将一棵满二叉树由上到下,由左至右,每个结点都用数字编号,另外一个二叉树也同样由上到下,由左至右,每个结点都用数字编号,二叉树中的每个结点都可以在满二叉树中一一对应,我们称这个二叉树为完全二叉树。所以一棵满二叉树一定是个完全二叉树,而完全二叉树不是满二叉树。
推论1: 对于位置为K的结点 左子结点=2k+1 右子结点=2(k+1)
推论2: 最后一个非叶节点的位置为 (N/2)-1,N为数组长度。
升序实例
将数组视为一个完全二叉树则是:
很明显,这个二叉树不符合最大二叉树的定义,需要初始化为最大堆,从最后一个非叶节点(8/2-1=3)开始,从下到上,从右到左调整
48和63调整到位后调整根节点35,将35与他的子节点86交换,此时86变成根节点,35变成子节点,此时35,11,63不符合二叉堆的定义,此时需要再次调整35的位置
此时完成了堆的初始化,最大的数已经成为了根节点
将堆顶的86和尾元素9交换
86现在处于数组下标为7的位置,不再将86视为二叉树的一部分,9归为根节点,此时需要重新调整元素的位置,使其重新变成二叉堆
继续将堆顶63和尾元素48交换,63现在处于数组下标为6的位置,不再将63视为二叉树的一部分。48处于根结点,很明显,此时需要调整元素的位置 使之重新变成二叉堆
经过反复将堆顶元素和尾元素交换,并调整二叉堆的过程,最后数据变为:
如果需要进行降序,改用最小堆即可。
代码实现
package sort;
import com.enjoyedu.PrintArray;
/**
* @author peiqi
* @Description: 堆排序
* @date 2021/10/1811:23
*/
public class HeapSort {
/**
* 声明全局变量,用于记录数组array的长度
*/
private static int len;
public static int[] sort(int[] array) {
len = array.length;
if (len < 1) {
return array;
}
//创建一个最大堆
buildMaxHeap(array);
//取出堆顶元素与尾元素交换
while(len>0){
swap(array,0,len-1);
len--;
//对剩下的元素重新调整为一个堆
adjustHeap(array,0);
}
return array;
}
private static void buildMaxHeap(int[] array) {
for (int i = (len/2)-1; i >=0; i--) {
adjustHeap(array,i);
}
}
/**
* 调整成为新的堆
* @param array
* @param i
*/
private static void adjustHeap(int[] array, int i) {
//保存最大的元素的索引
int maxIndex = i;
//左节点
int left = 2*i+1;
//右节点
int right = 2*(i+1);
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (left<len&&array[left]>array[maxIndex]){
maxIndex = left;
}
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (right < len && array[right] > array[maxIndex]) {
maxIndex = right;
}
//如果父节点不是最大值
if (maxIndex!=i){
//则将父节点与最大值交换
swap(array,maxIndex,i);
//递归调整与父节点交换的位置。
adjustHeap(array,maxIndex);
}
}
/**
* 交换数组内两个元素
* @param array
* @param i
* @param j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
计数排序
计数排序是一个排序时不比较元素大小的排序算法,对一定范围内的整数排序时速度非常快,但局限性较大,只限于对整数进行排序,而且待排序元素值分布较连续跨度小的情况
升序实例
1.初始化一个大小为(5+1)的计数数组(所有元素初始值为0)遍历整个原始数组将原始数组中的每个元素对应计数数组下标的元素大小+1
2.遍历计数数组,将对应位置的值改成对应计数数组中的元素的值
代码实现
package sort;
import java.util.Arrays;
/**
* @author peiqi
* @Title:
* @Package
* @Description:
* @date 2021/10/1819:59
*/
public class CountingSort {
public static int[] sort(int[] array){
if (array.length==0){
return array;
}
//寻找数组中的最大值,最小值
int min = array[0];
int max = array[0];
/**
* bias偏移量
* 用于定位原始数组每个元素在计数数组中的下表位置
*/
int bias;
for (int i = 1; i < array.length; i++) {
if (array[i]>max){
max = array[i];
}
if (array[i]<min){
min = array[i];
}
}
bias = 0-min;
//获得计数数组的容量
int[] counterArray = new int[max-min+1];
Arrays.fill(counterArray,0);
/**
* 遍历整个原始数组,将原始数组中每个元素值转化为计数数组下标
* 并将计数数组下标对应的元素值大小累加
*/
for (int i = 0; i <array.length ; i++) {
counterArray[array[i]+bias]++;
}
//访问原始数组时的下标计数器
int index = 0;
//访问计数数组的下标计数器
int i = 0;
/*访问计数数组,将计数数组中的元素转换后,重新写回原始数组*/
while (index<array.length){
/*只要计数数组中当前下标元素的值不为0,就将计数数组中的元素转换后,重新写回原始数组*/
if (counterArray[i]!=0){
array[index] = i-bias;
counterArray[i]--;
index++;
}else{
i++;
}
}
return array;
}
}
找到最大值最小值是为了节省空间,如果待排序数组的元素跨度很大计数排序就不再适合
桶排序
利用某种函数的映射关系将数据分到有限数量的桶中,每个桶再分别排序
- 根据输入建立适当个数的桶,每个桶可以存放某个范围的元素
- 将特定范围内的所有元素放入对应的桶中
- 对非空的桶中的元素进行排序
- 按照划分的范围顺序,将桶中的元素依次取出。排序完成。
升序实例
我们可以建立5个桶,每个桶按照范围顺序依次是[0, 10)、[10, 20)…[40, 49)
对这5个桶的元素分别排序,依次取出桶中的元素得到排序后的序列
代码实现
package sort;
import java.util.ArrayList;
/**
* @author peiqi
* @Title:
* @Package
* @Description:
* @date 2021/10/2015:32
*/
public class BucketSort {
/**
*
* @param array
* @param bucketSize 作为每个桶所能放置多少个不同数值
* (例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,
* 但是容量不限,即可以存放100个3);
* @return
*/
public static ArrayList<Integer> sort(ArrayList<Integer> array,int bucketSize){
if (array==null||array.size()<2){
return array;
}
//找到最大最小值
int max = array.get(0);
int min = array.get(0);
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max) {
max = array.get(i);
}
if (array.get(i) < min) {
min = array.get(i);
}
}
//获取桶的数量
int bucketCount = (max-min)/bucketSize+1;
//构建桶
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i <bucketCount ; i++) {
bucketArr.add(new ArrayList<Integer>());
}
//将原始数组中的数据分配到桶中
for (int i = 0; i <array.size() ; i++) {
bucketArr.get((array.get(i)-min)/bucketSize).add(array.get(i));
}
for (int i = 0; i <bucketCount ; i++) {
//桶中只有一个元素,就无需再排序
if (bucketSize==1){
for (int j = 0; j <bucketArr.get(i).size() ; j++) {
resultArr.add(bucketArr.get(i).get(j));
}
}else{
if (bucketCount==1){
bucketSize--;
}
//对桶中的数据再次进行桶排序
ArrayList<Integer> temp = sort(bucketArr.get(i),bucketSize);
for (int j = 0; j <temp.size() ; j++) {
resultArr.add(temp.get(j));
}
}
}
return resultArr;
}
}
基数排序
常见的数据元素一般是由若干位组成的,基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,同时每一轮排序都基于上轮排序后的结果;当我们将所有的位排序后,整个数组就达到有序状态。比如对于数字2985,从右往左就是先以个位为关键字进行排序,然后是十位、百位、千位,总共需要四轮。基数排序不是基于比较的算法。
对于十进制整数,每一位都只可能是0~9中的某一个,总共10种可能。那10就是它的基,同理二进制数字的基为2;对于字符串,如果它使用的是8位的扩展ASCII字符集,那么它的基就是256。
升序实例
首先按个位排序
然后按十位排序
代码实现
package sort;
import java.util.ArrayList;
/**
* @author peiqi
* @Title:
* @Package
* @Description:
* @date 2021/10/2016:24
*/
public class RedixSort {
public static int[] sort(int[] array){
if (array==null||array.length<2){
return array;
}
//找出最大数
int max = array[0];
for (int i = 1; i <array.length ; i++) {
max = Math.max(max,array[i]);
}
//先算出最大数的位数
int maxDigit = 0;
while(max!=0){
max/=10;
maxDigit++;
}
int mod = 10,div = 1;
//构建桶
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i <10 ; i++) {
bucketList.add(new ArrayList<Integer>());
}
/**
* 按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,
* 每一轮排序都基于上轮排序后的结果
*/
for (int i = 0; i <maxDigit ; i++,mod*=10,div*=10) {
//遍历原始数组,投入桶中
for (int j = 0; j <array.length ; j++) {
int num = (array[j]%mod)/div;
bucketList.get(num).add(array[j]);
}
// 桶中的数据写回原始数组,清除桶准备下一轮的排序
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k <bucketList.get(j).size() ; k++) {
array[index++] = bucketList.get(j).get(k);
}
bucketList.get(j).clear();
}
}
return array;
}
}
基数排序:根据键值的每个数字来分配桶,计数排序:每个桶只存储单一键值,桶排序:每个桶存储一定范围的数值
排序总结
算法的稳定性:
- 稳定:如果a原本在b前面,而a=b,排序之后a任然在b的前面
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面
由小到大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
排序算法时间复杂度助记
- 冒泡、选择、插入排序需要2个for循环,每次只关注一个元素,平均时间复杂度为O(n^2)
- 快速、归并、希尔、堆基于分治思想平均复杂度往往是O(nlogn)
- 基数排序时间复杂度为O(N*M),其中N为数据个数,M为数据位数