0、算法概述
0.1、算法分类
十种常用的排序算法可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
0.2、算法复杂度
0.3、相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的前面。
- 时间复杂度:对排序数据的总的操作次数。反映当 n 变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内存执行时所需要存储空间的度量,它也是数据规模 n 的函数。
1、冒泡排序(Bubble Sort)
,冒泡排序是一种简单的排序算法。它重复地走过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说数列已经排序完成。这个算法的名字由来是因为越小地元素会经由交换慢慢浮到数列的前端。
1.1、算法描述
- 比较相邻的元素。比第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
增强的冒泡排序:
思路:因为排序过程中,各个元素不断接近自己的位置,如果一趟比较下来没有进行过交换,则说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换,从而减少不必要的比较。
1.2、动图演示
1.3、代码实现
//冒泡排序的外层循环代表需要向后移动的多少个最大的数
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]) {
flag=true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if(!flag){ //在一趟排序中 一次交换都没有发生过
break;
}else{
flag=false; //重置flag 进行下次判断
}
}
2、选择排序(Selection Sort)
选择排序(Selection - sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)的元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)的元素,然后放到已经排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1、算法描述
n个记录的直接选择排序可经过n-1躺直接排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为 R[1…n],有序区为空;
- 第 i 趟排序 (i=1,2,3 … n-1)开始的时候,当前有序区和无序区分别为 R[1 … i-1]和R[i … n]。该趟排序从当前无序区中选出关键字最小的记录R[k],将它于无序区的第1个记录R交换,使R[1 … i]和R[i+1 … n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1 趟结束,数组有序化了。
2.2、动图演示
2.3、代码实现
int temp=0;
/**
* 注意 选择排序是指得向后移动元素 放到末尾
*/
for (int i = 0; i < arr.length-1; i++) {
for (int j = i+1; j < arr.length; j++) {
if(arr[i]>arr[j]){
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
}
2.4 算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
3、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后往前扫描,找到相应位置并插入。
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式寻找该元素的适当位置,以达到排序的目的。
3.1、算法描述
把 n 个待排序的元素看成为一个有序表和一个无序表,开始的时候有序表中只包含一个元素,无序表中包含 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素排序码进行比较(从后向前进行比较),将它插入到有序表中的适当位置,使之成为新的有序表。
3.2、代码实现
public static void insertSort(int[] arr){
// 使用逐步推导的方法来进行讲解 便于理解
// 第一轮 {101,34,119,1,72} => {34,101,119,1,72}
// 外层循环代表要插入几次 除了第一个元素为有序表以外 其他都是无序表中的元素应该全部进行插入
for (int i=1; i < arr.length; i++){
// 保存要插入的数据
int insertValue = arr[i];
int insertIndex = i-1; // 定义待插入位置的下标,即待插入前面的这个数字的下标
//给insertValue找到插入的位置
/**
* 注意:
* 1/ insertIndex>=0 是为了找到插入的位置 退出循环的条件有两种:
* 1> 当insertIndex==0的时候 当其还满足要插入的值小于数组中下标为0的元素 那么在循环中要插入下标为-1
* 此时下标越界 退出循环 要插入的位置就是该insertIndex后面的那个下标
* 2> 下标不越界 满足inserValue>=arr[insertIndex]的时候 要插入的也应该是其后面下标的那个位置
* 因为这个下标值对应的刚好是大于等于要插入元素的值 不能插入该位置
*/
while(insertIndex >= 0 && insertValue < arr[insertIndex]) // 保证给insertValue找插入位置的时候不越界
{
arr[insertIndex+1] = arr[insertIndex];
insertIndex--;
}
// 注意 此时 insertIndex+1才是我们要插入的位置 当inserIndex+1!=i的时候 说明insertValue不是前面有序队列中最大的元素
// 不能放在其序列最后
if(insertIndex+1 != i){
arr[insertIndex+1] = insertValue;
}
}
}
4、希尔排序(Shell Sort)
希尔排序也是一种插入排序,它是简单的插入排序经过改进之后的一种更高效的版本,也称为缩小增量排序。
希尔排序是把记录按照下标的一定增量进行分组,对每组使用直接插入排序算法排序,随着增量的减少,每组包含的关键词越来越多,当增量减少到1的时候,整个文件恰好被分成一组,算法便终止。
4.1、算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1、t2、…,tk,其中 ti > tj,tk=1;
- 按照增量序列的个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序,仅仅因为增量因子为 1 的时候,整个序列作为一个表来处理,表长度为整个序列的长度。
4.2、动图演示
4.3、代码实现
// 对交换式的希尔排序进优化 -> 移位法
public static void shellSort3(int[] arr){
// 增量gap 并逐步的缩小增量
for(int gap = arr.length/2; gap > 0; gap /= 2){
// 从第 gap 个元素 逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int insertValue = arr[i]; // 要插入的元素
int insertIndex = i - gap; // 定义待插入位置的下标,即待插入前面的这个数字的下标
while (insertIndex >= 0 && insertValue < arr[insertIndex]){
arr[insertIndex + gap] = arr[insertIndex];
insertIndex -= gap;
}
if(insertIndex + gap != i){
arr[insertIndex + gap] = insertValue;
}
}
}
}
5、归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法( Divide and Conquer)的一个典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表个秉承一个有序表,成为2 - 路归并。
动图演示
代码实现
/**
* @author wcc
* @date 2021/6/16 8:51
* 归并排序:
* 归并排序是利用归并的思想实现的排序的方法 该算法采用经典的分治策略(分治问题将问题分为一些小的问题然后递归求解)
* 而治的阶段则将分的阶段得到的答案修补在一起 即分而治之
*/
public class MergeSort {
public static void main(String[] args) {
int [] arr={8,4,5,7,1,3,6,10,2,9};
int[] temp=new int[arr.length]; //归并排序需要一个额外的空间
mergeSort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(arr));
}
//分+合的方法
public static void mergeSort(int[] arr,int left,int right,int[] temp){
if(left<right){ //注意 这里归并排序要分成一个一个的 所以这里是< 不能带= 注意
int mid=(left+right)/2; //中间的索引
//向左递归进行分解
mergeSort(arr,left,mid,temp); //向左分解
mergeSort(arr,mid+1,right,temp);
mergeSort(arr,left,mid,right,temp);
}
}
//合并的方法
/**
* @param arr 需要排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边有序系列的最后的索引
* @param temp 中转数组
*/
public static void mergeSort(int[] arr,int left,int mid,int right,int[] temp){
int i=left; //初始化i 左边有序序列的初始索引
int j=mid+1; //初始化j 右边有序序列的初始索引
int t=0; //指向temp临时数组的当前索引
//1、先把左右两边的数据按照规则填充到temp数组中
//直到左右两边的有序序列 有一边处理完毕为止
while(i<=mid && j<=right){ //那么便填充临时数组temp
//如果左边的有序序列的当前元素 小于等于右边序列的当前元素
//即将左边的当前元素拷贝到临时数组temp中
//然后t要往后移动 i也要往后进行移动
if(arr[i]<=arr[j]){
temp[t]=arr[i];
i++;
t++;
}else{ //反之 将右边有序序列的当前元素填充到临时数组temp中
temp[t]=arr[j];
j++;
t++;
}
}
//2、把有剩余数据的一边的数据依次全部填充到temp
while(i<=mid){ //说明左边的有序序列还有剩余的元素 就全部填充到temp中
temp[t]=arr[i];
i++;
t++;
}
while (j<=right){
temp[t]=arr[j];
t++;
j++;
}
//3、将temp数组的元素拷贝的原数组arr
//注意 并不是每次都拷贝所有的元素 因为归并排序在合的时候一共会合并数组元素个数-1次
t=0;
int tempLeft=left;
//第一次合并 tempLeft=0 right=1 // tempLeft=2 right=3 //tempLeft=0 right=3
//最后一次合并 tempLeft=0 right=7
while(tempLeft<=right){
arr[tempLeft]=temp[t];
t++;
tempLeft++;
}
}
}
算法分析
归并排序是一种稳定的排序方法,和选择排序一样,归并排序的性能不受输入数据的影响,但是表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
6、快速排序(Quick Sort)
快速排序的介绍:快速排序是对冒泡排序的一种改进,基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按照此方法对这两部分的数据分别进行快速排序,整个排序过程就可以进行递归进行,以此达到整个数据变成有序序列。
动图演示
代码实现
public class QuickSort {
public static void main(String[] args) {
int[] arr={-9,78,23,-2,-7,0,25,-567,-8,0,-23,70};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right){
if (left<right){
//获取基准数据的索引
int index=getIndex(arr,left,right);
quickSort(arr,left,index-1);
quickSort(arr,index+1,right);
}
}
//获取基准数据的正确索引
private static int getIndex(int[] arr,int low,int high){
//记录基准数据 为数组的头部
int temp=arr[low];
while(low<high){ //外面这个循环是不加循环的话只能找到一组数据 可能有多组
while (low<high && arr[high]>=temp){
high--;
}
arr[low]=arr[high];
while (low<high && arr[low]<=temp){
low++;
}
arr[high]=arr[low];
}
//当退出循环的时候 此时low==high
//此时low和索引指向的位置就是基准数据应该所处的位置
arr[low]=temp;
return low;
}
}
public class QuickSort2 {
public static void main(String[] args) {
int[] arr={-9,78,0,23,0,9,0,-567,70};
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right){
int l=left; //左索引
int r=right; //右索引
int temp=0; //临时变量
//pivot 中轴
int pivot=arr[(l+r)/2];
//while循环的目的是让比pivot值小的放到左边
//比pivot值大的放到右边
while(l<r){
//在pivot的左边一直找 找到大于等于pivot的值才退出
while(arr[l]<pivot){
l++;
}
//在pivot的右边一直找 找到小于等于pivot的值才退出
while(arr[r]>pivot){
r--;
}
//如果l>=r成立 说明pivot的左右两边的值已经按照左边全部是小于等于pivot值
//右边全部都是大于等于pivot值
if(l==r){
break;
}
//交换
temp=arr[l];
arr[l]=arr[r];
arr[r]=temp;
//判断 这里是为了防止交换后的两个数都等于pivot 之后出现死循环 两个数一直在交换
if(arr[l]==pivot){
r--;
}
if(arr[r]==pivot){
l++;
}
}
//因为退出循环的时候 注意 l>=r 所以这里必须l++ r-- 否则会出现栈溢出的现象
if(l==r){
l++;
r--;
}
//向左递归 这里一定要进行判断 不然会出现栈溢出的情况
if(left<r){ //注意 这里left==r的时候是只有一个元素 所以不需要递归
quickSort(arr,left,r);
}
if(right>l){
quickSort(arr,l,right);
}
}
}
7、堆排序(Heap Sort)
算法描述
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的平均时间复杂度为 O(nlogn),它也是不稳定的排序。
- 堆是具有以下性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆,注意,没有要求节点的左孩子节点的值和右孩子节点的值有大小关系
- 每个节点的值都小于等于其左右孩子节点,称为小顶堆。
堆排序的基本思想是:
- 将待排序的序列构造成一个大顶堆(以数组的形式存在)
- 此时,整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余 n-1 个元素重新构成一个
动图演示
]
代码实现
public class HeapSort {
public static void main(String[] args) {
int[] arr={4,6,8,5,9,-1,54,67,43,-8};
heapSort(arr);
}
//编写一个堆排序的方法
public static void heapSort(int[] arr){
System.out.println("堆排序");
/*//分步完成
adjustHeap(arr,1,arr.length);
System.out.println(Arrays.toString(arr));
adjustHeap(arr,0,arr.length);
System.out.println(Arrays.toString(arr));*/
//完成最终代码
//将无序序列构建成一个堆 根据升序降序需求选择大顶堆或者小顶堆
//完全二叉树非叶子节点的个数 arr.length/2-1 为完全二叉树中非叶子节点的个数
//这一个循环必须要有 是为了将给该数组所对应的二叉树构造成一个大顶堆
// 将无序序列构建成一个堆 根据升序降序的需求选择大顶堆或者小顶堆
// 完全二叉树 非叶子节点的个数 arr.length/2-1 为完全二叉树中非叶子节点的个数
// 这一个循环必须要有 是为了将该数组所对应的二叉树构造成一个大顶堆
for (int i = arr.length/2-1; i >=0 ; i--) {
adjustHeap(arr,i,arr.length);
}
//System.out.println(Arrays.toString(arr));
/**
* 将堆顶元素与末尾元素交换 将最大元素沉到数组末端
* 重新调整结构 使其满足堆定义 然后继续交换堆顶元素与当前末尾元素
*/
for (int j = arr.length-1; j > 0; j--) {
//交换
arr[j]=arr[0]+(arr[0]=arr[j])*0;
//这里为什么是i 因为此时的大顶堆已经构建完毕 我们只需要从上到下调整推就行了 不用重新构建大顶堆 所以 这里是只从0开始
adjustHeap(arr,0,j);
}
System.out.println(Arrays.toString(arr));
}
// 将一个数组(二叉树),调整成一个大顶堆
/**
* 功能:完成将以i对应的非叶子节点的树调整成大顶堆
* 举例:int arr[] = {4,6,8,5,9} => i=1 ==> adjustHeap => 得到 {4,9,8,5,6}
* 如果我们再次调用 adjustHeap 传入的是 i=0 ==> 得到 {4,9,8,5,6} => {9,6,8,5,4}此时已经调整为大顶堆
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 表示对多少个元素进行调整 length是在逐渐减少
*/
public static void adjustHeap(int[] arr,int i,int length){
int temp = arr[i]; // 先取出当前元素的值,保存在临时变量
// 开始调整
// 说明
// 1. k = i * 2 + 1 k是i节点的左子节点
for (int j = i * 2 + 1; j < length; j = j * 2 + 1) {
if(j+1 < length && arr[j] < arr[j+1]){ // 说明左子节点的值小于右子节点的值
j++; // j 指向右子节点
}
if(arr[j] > temp){ // 如果子节点大于父节点
arr[i] = arr[j]; // 把较大的值赋值给父节点
i = j; // i 指向 j,继续循环比较
}else{
// 注意 这里为什么是直接退出循环,因为这里构建大顶堆的过程是按照从左到右 从上到下的过程
// 进行构建的,每个非叶子节点对应的树都是大顶堆,当传入的非叶子节点大于其左右子节点的时候,就不用在去判断其他叶子节点了
break;
}
}
// 当for循环结束后 我们已经将以i为父节点的树的最大值放在了i原先的位置 局部大顶堆
arr[i] = temp;
}
}
8、基数排序(Counting Sort)
算法描述
- 属于分配式排序,它是通过键值的各个位的值,将要排序的元素分配到某些桶中,达到排序的作用
- 基数排序属于稳定性的排序,技术排序法是效率高的稳定性排序法
- 基数排序是桶排序的扩展
- 基数排序是这样实现的:将整数位按照位数切割成不同的数字,然后按照每个位数分别进行比较
基数排序的基本思想
将所有待比较的数值统一为同样的数位长度,数位较短的数前面补0,然后从最低位开始依次i进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成了一个有序序列
注意:需要几轮排序跟我们数组中最大数的位数相等
动图演示
代码实现
//基数排序算法
private static void redisxSort(int[] arr){
//定义一个二维数组表示10个桶 每个桶就是一个一维数组
//1.二维包含10个一维数组 对应的是0-10 表示每一位可能会对应的数
//2.为了防止在放数的时候 数据溢出 每个一维数组的大小为arr.length
//很明显 基数排序是使用空间换时间的经典算法
int[][] bucket=new int[10][arr.length];
//为了记录每个桶中实际存放了多少个数据 我们定义一个一维数组来记录每个桶中放入数据的个数
int[] bucketElementCounts=new int[10];
//根据前面的推导过程 我们可以得到最终的基数排序的代码
//1.先得到数组中最大数的位数
int max=arr[0]; //假设中数组中第一个数为最大值
for (int i = 0; i < arr.length; i++) {
if(max<Math.abs(arr[i])){
max=Math.abs(arr[i]);
}
}
for (int i = 0; i < arr.length; i++) {
arr[i]+=max;
}
int tempMax=max; //用以保存原来数组中绝对值最大的值 用以排序好新数组以后得到原来数组中的数值
for (int i = 0; i < arr.length; i++) { //得到新数组中的最大值
if(max<arr[i]){
max=arr[i];
}
}
//得到最大数的位数
int maxLength=(max+"").length();
for (int gap=0; gap<maxLength; gap++){ //外层循环的次数是数组中最大数的位数
//针对每个元素的对应位的值进行排序处理 第一次是个位 第二次是十位 第三次是百位
for (int i = 0; i < arr.length; i++) {
//取出每个元素的个位
int digitOfElement=(arr[i]/(int)(Math.pow(10,gap)))%10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]]=arr[i];
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序 (一维数组的下标依次取出数据 放入原来的数组)
int index=0; //代表原数组的下标 用以记录放入数据的时候的下标
//遍历每一个桶 并将桶中的数据放入到原数组
for (int i = 0; i < bucketElementCounts.length; i++) { //i对应的是桶所对应的下标
if(bucketElementCounts[i]!=0){ //说明对应的二维数组中桶中有元素
//循环该桶 即第i个一维数组 放入
for (int j = 0; j < bucketElementCounts[i]; j++) { //j代表的是该桶中的数据的个数
//取出数据放入到arr原数组中
arr[index]=bucket[i][j];
index++;
}
}
//取出数据 之后 应该把bucketElementCounts[i]=0 应该把该位上的值置为0 因为每次基数排序桶中的数据都是不一样的
//这个记录桶中数据的个数应该置为空 重新在下一轮放入数据的时候开始计数
bucketElementCounts[i]=0;
}
}
for (int i = 0; i < arr.length; i++) {
arr[i]-=tempMax;
}
}
算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但是基数排序的性能要比桶排序要略差,每一次关键的桶分配都需要 O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假设待排序数据可以分为 D 个关键字,则基数排序的时间复杂度将是O(D * 2n),当然 D 要远远小于 n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。