冒泡排序
定义
重复地走访过要排序的元素,依次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。
走访元素的工作是重复地进行直到没有相邻元素需要交换 即该元素列已经排序完成
稳定性
冒泡排序是一种稳定排序算法
时间复杂度
O(N2)
代码
测试用例
int arr[] = {2,4,3,1,6,5};
原代码
/**
* 普通冒泡排序
* 缺陷:耗时 每次都会无条件遍历
* @param arr
* @param len arr.length
*/
public static void bubbleSort(int arr[],int len){
for (int i = 0;i<len-1;i++){
System.out.println();
System.out.println("第"+(i+1)+"轮");
for (int j=0;j<len-1-i;j++){
if (arr[j]>arr[j+1]){
// 交换
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
System.out.println("第"+(i+1)+"_"+(j+1)+"交换:" + Arrays.toString(arr));
}
}
}
运行结果
第一次优化
第一次优化:当第n次没有数据交换时,结束循环
/**
* 冒泡排序
* 第一次优化:当第n次没有数据交换的时候 结束循环
*
* @param arr
* @param len arr.length
*/
public static void bubbleSortOptimize1(int arr[],int len){
System.out.println("第一次优化:\n记录本轮循环是否发生了交换\n\n");
boolean swap = true;
// 也可以写成 for (int i = 0;i<len-1&&swap;i++) 来避免多一次循环
for (int i = 0;i<len-1;i++){
// 上一轮没有发生交换 表示已经排序好 跳出此层循环 (结束排序)
if (!swap){
break;
}
System.out.println("第"+(i+1)+"轮");
swap = false;// 如果下面有数据交换的话 改为true
for (int j=0;j<len-1-i;j++){
if (arr[j]>arr[j+1]){
// 交换
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
swap = true; // 这一轮发生了交换 表示数据可能没有排好
}
System.out.println("第"+(i+1)+"_"+(j+1)+"交换:" + Arrays.toString(arr));
}
}
}
运行结果
第二次优化
每次一发生交换时,记录交换的位置,下面在循环的时候,只需要比较到上次最后两两交换的位置即可
/**
* 冒泡排序
* 第二次优化:记录最后交换的位置减少一轮中遍历次数 当第n次没有数据交换的时候 结束循环
* @param arr
* @param len arr.length
*/
public static void bubbleSortOptimize2(int[] arr,int len){
System.out.println("第二次优化:\n记录最后交换的位置减少一轮中遍历次数\n当第n次没有数据交换的时候 结束循环\n\n");
// 记录是否需要交换
boolean swap = true;
int swappedIndex = -1;// 记录上次冒泡过程中发生交换的位置
int lastIndex = len -1;// 定义内层循环条件:上一次最后发生交换的位置
int i = 0;// 记录是第几次循环
while(swap && lastIndex!=0){
i++;
System.out.println("第"+i+"轮");
// 假定不需要交换
swap = false;
for (int j = 0;j<lastIndex;j++){
if (arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swap = true;
swappedIndex = j;
}
System.out.println("第"+i+"_"+(j+1)+"交换:" + Arrays.toString(arr));
}
lastIndex = swappedIndex;
System.out.println("最后交换的位置:"+lastIndex+"\n");
}
}
运行结果
交换函数
/**
* 交换函数
* @param arr 数组 [引用数据类型]
* @param i 下标
* @param j 下标
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
扩展:异或运算
异或运算是一种位运算, 相同则为0 不同则为1
异或运算满足交换律,a异或b两次,得到的是a本身
交换数值:
num1 ^= num2;
num2 ^= num1;
num1 ^= num2;
前提是num1不等于num2,否则结果是0,有相同数值排序的时候不可以用异或
选择排序
定义
从待排序的数据元素中选出最小(或最大的一个元素),存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。
以此类推,直到全部待排序的数据元素的个数为0.
稳定性
不稳定
时间复杂度
O(N2)
代码
测试用例
int arr[] = {2,4,3,1,6,5};
交换函数
/**
* 交换函数
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
原代码
/**
* 选择排序
* @param arr
* @param len
*/
public static void selectSort(int arr[],int len){
int minIndex; // 记录最小元素的下标
// 极限是倒数第二个元素
for (int i = 0;i<len-1;i++){
System.out.println("\n第"+(i+1)+"轮");
minIndex = i;
// 极限是最后一个元素
for (int j = i+1;j<len;j++){
if (arr[minIndex]>arr[j]){
minIndex = j; // 更新最小元素下标
System.out.println("较小元素:["+arr[minIndex]+"]; 较小元素下标:"+minIndex);
}
}
// 将较小的元素放到前面 达到排序目的
swap(arr,i,minIndex);
System.out.println("第"+(i+1)+"次交换:" + Arrays.toString(arr));
}
}
运行结果
优化
每轮遍历会找出最大值和最小值 然后将其放到按照排序应放置的位置,下一轮循环的时候,已经处理过的值(首尾)不会再被遍历到
以i
记录每次开始遍历的位置 前i个
和后i个
都不会被遍历到,因为在上轮遍历中已经有序
这样可以减少将近一半的循环遍历
/**
* 选择排序
* 第一次优化
* 找出最大最小值
* 每轮循环都会将首尾排好
* @param arr
* @param len
*/
public static void selectSortOptimize1(int arr[],int len){
System.out.println("初始状态 " + Arrays.toString(arr));
int minIndex = 0; // 记录较小值的下标
int maxIndex = 0; // 记录较大值的下标
// 极限是倒数第二个元素
for (int i = 0;i<len/2;i++){
System.out.println("\n第"+(i+1)+"轮");
minIndex = i; // 本轮较小元素目标位置
maxIndex = len-1-i; // 本轮较大元素目标位置
// 极限是最后一个元素
for (int j = i+1;j<len-i;j++){
if (arr[minIndex]>arr[j]){
minIndex = j; // 更新较小元素下标
}
if (arr[maxIndex]<arr[j]){
maxIndex = j; // 更新较大元素下标
}
}
System.out.println("较小元素:["+arr[minIndex]+"]; 较小元素下标:"+minIndex);
System.out.println("较大元素:["+arr[maxIndex]+"]; 较大元素下标:"+maxIndex);
swap(arr,i,minIndex); // 将较小的元素放到前面 达到排序目的
swap(arr,len-1-i,maxIndex); // 将较大的元素放到后面 达到排序目的
System.out.println("第"+(i+1)+"次交换:" + Arrays.toString(arr));
}
}
运行结果
插入排序
定义
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的,个数加一的有序数据,直到全部插入完为止。
适用于少量数据的排序
稳定性
稳定
时间复杂度
O(N2)
代码
测试用例
int arr[] = {2,4,3,1,6,5};
原代码
/**
* 直接插入排序
* @param arr
* @param len arr.length
*/
public static void insertSort(int[] arr,int len){
System.out.println("初始数据 "+ Arrays.toString(arr));
// i用作下标;i从1开始,因为一个元素就是有序的 不需要再排
for(int i=1;i<len;i++){
System.out.println("\n第"+i+"轮");
int temp = arr[i]; // 防止数据丢失 为下面后移元素做准备
System.out.println("哨兵:"+temp);
// 倒着比较,每次加入一个元素和左侧已经排序好的序列作比较
// 左侧元素比temp大的时候 为temp腾位置 j接着左移
int j;
for (j=i;j>0 && arr[j-1]>temp; j--){
arr[j] = arr[j-1];
System.out.println("第"+i+"_"+(i-j+1)+"次交换:" + Arrays.toString(arr));
}
//跳出循环的时候 要么j==0 要么arr[j-1]<=temp
//j ==0 表示temp为最小值; arr[j-1]<=temp 表示temp介于排好的最大最小之间
arr[j] = temp;
System.out.println("第"+i+"轮交换结果:" + Arrays.toString(arr));
}
}
运行结果
优化 [希尔排序]
希尔排序是插入排序的一种,又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法;随着增量逐渐减少,每组包含的关键词就会越来越多,当增量减至1时,整个文件恰被分为一组,算法 完成
缺点
代码可读性差
重点
选择增量
稳定性
不稳定
时间复杂度
O(N1.3)
/**
* 希尔排序
* 插入排序的优化 用的少 代码可读性差
* 重点:选择增量 初始值为 n/2 减少了移动的次序和比较的次数
* 时间复杂度(O(n^1.3))
* @param arr
* @param len
*/
public static void shellSort(int[] arr,int len){
System.out.println("初始数据 " + Arrays.toString(arr));
int out=1; // 用于统计外层循环次数
for (int d = len/2;d>0;d/=2){
System.out.println("\n\n第"+(out++) +"次分组 "+ "分组步长:"+d);
int in = 1;//用于统计内层循环
for (int i=d;i<len;i++){
System.out.println("第"+in +"轮");
int temp = arr[i]; // 即某个组的第二个元素
int j;
// 一轮for循环进行的是多组排序
for (j=i-d;j>=0&&temp<=arr[j];j-=d){
arr[j+d] =arr[j];
}
arr[j+d]=temp;
System.out.println("第"+(in++) +"轮结果 "+Arrays.toString(arr));
}
}
}
运行结果
归并排序
定义
归并排序是采用分治法的一个非常典型的应用,将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序
若将两个有序表合并成一个有序表,称为二路归并
稳定性
稳定
时间复杂度
O(N*logN)
代码
测试用例
int arr[] = {2,4,3,1,6,5};
原代码
/**
* 归并排序(重载)
* 为了创建临时数组 完成数组的复用
*
* @param arr 无序数组
*/
public static void mergeSort(int[] arr){
System.out.println("初始数据" + Arrays.toString(arr));
int[] temp = new int[arr.length];
mergeSort(arr,temp,0,arr.length-1);
}
/**
* 归并排序
* @param arr
* @param temp
* @param start
* @param end
*/
public static void mergeSort(int[] arr,int[] temp,int start,int end){
if (start >= end){
return;
}
int mid = (start + end) / 2;
// 排序第一个数组
mergeSort(arr,temp,start,mid);
// 排序第二个数组
mergeSort(arr,temp,mid+1,end);
// 合并两个有序数组
merge(arr,temp,start,mid,end);
}
运行结果
(输出语句见 二路归并 算法)
扩展1 二路归并
/**
* 二路归并 合并两个有序序列
* 现将两个有序序列合并成一个序列 然后开始排序
* @param arr 存放两个有序列表的数组
* @param temp 临时数组
* @param start 左侧有序列表开始的下标
* @param mid 左侧结束位置下标 右侧开始位置下标:mid+1
* @param end 右侧有序列表结束下标
*/
public static void merge(int[] arr,int[] temp,int start,int mid,int end){
System.out.println("前一组开始下标 "+start);
System.out.println("前一组结束下标 "+mid);
System.out.println("后一组开始下标 "+(mid+1));
System.out.println("后一组结束下标 "+end);
int i = start; // i 用来记录 第一个数组的遍历位置
int j = mid + 1; // 用来记录第二个数组的遍历位置
int k = start; // 用来表示临时数组的下标
// i=mid+1:第一个数组先遍历完成
// j=end+1:第二个数组先遍历完成
while (i != mid +1 && j != end +1){
if (arr[i]>arr[j]){
temp[k++] = arr[j++];
}else {
temp[k++] = arr[i++];
}
}
// 第一个数组没有遍历完毕
while (i != mid + 1){
temp[k++] = arr[i++];
}
// 第二个元素没有遍历完毕
while (j != end + 1){
temp[k++] = arr[j++];
}
for (int n = start;n<=end;n++){
arr[n] = temp[n];
}
System.out.println("本次合并结果:"+Arrays.toString(arr));
}
扩展二 递归的时间复杂度
master公式
T(N) = a * T( N b {N\over b} bN ) + O(Nd)
- N:数据样本量
- a:子过程调用次数
- N b {N\over b} bN:子过程数据样本量
- O(Nd):除去递归之外的时间复杂度
递归时间复杂度
扩展三 应用
数组小和问题
题面
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和
示例
[2,3,4,1,5] 小和为17
- 2左边比其小的数:
- 3左边比其小的数:2
- 4左边比其小的数:2 3
- 1左边比其小的数:
- 5左边比其小的数:2 3 4 1
思路
求数组中每个数左侧比其小的所有数的和,等同于求每个数右侧比其大的数的数量乘以这个数的和进行累加
代码实现
/**
* 求数组小和
* @param arr
*/
public static void minSum(int[] arr){
if (arr == null || arr.length<2){
System.out.println("所求小和为 0");
return;
}
int[] temp = new int[arr.length];
int minsum = mergeSort4MinSum(arr,temp,0,arr.length-1);
System.out.println("所求小和值为 "+minsum);
}
/**
* 求数组小和
* @param arr
* @param temp
* @param start
* @param end
* @return
*/
public static int mergeSort4MinSum(int[] arr,int[] temp,int start,int end){
if (start >= end){
return 0;
}
int mid = (start + end) / 2;
// 排序第一个数组 计算第一个数组小和
int leftMinSum = mergeSort4MinSum(arr,temp,start,mid);
// 排序第二个数组 计算第二个数组小和
int rughtMinSum = mergeSort4MinSum(arr,temp,mid+1,end);
// 合并两个有序数组 计算总的数组小和
int allMinSum = merge4MinSum(arr,temp,start,mid,end);
return leftMinSum + rughtMinSum +allMinSum;
}
/**
* 求数组小和
* @param arr 存放两个有序列表的数组
* @param temp 临时数组
* @param start 左侧有序列表开始的下标
* @param mid 左侧结束位置下标 右侧开始位置下标:mid+1
* @param end 右侧有序列表结束下标
* @return 本组小和
*/
public static int merge4MinSum(int[] arr,int[] temp,int start,int mid,int end){
System.out.println("前一组开始下标 "+start);
System.out.println("前一组结束下标 "+mid);
System.out.println("后一组开始下标 "+(mid+1));
System.out.println("后一组结束下标 "+end);
int i = start; // i 用来记录 第一个数组的遍历位置
int j = mid + 1; // 用来记录第二个数组的遍历位置
int k = start; // 用来表示临时数组的下标
int minSumResult = 0;
// i=mid+1:第一个数组先遍历完成
// j=end+1:第二个数组先遍历完成
while (i != mid +1 && j != end +1){
if (arr[i]>arr[j]){
temp[k++] = arr[j++];
}else {
minSumResult += arr[i]<arr[j]?(end-j+1) * arr[i]:0; // 计算小和语句
temp[k++] = arr[i++];
}
}
// 第一个数组没有遍历完毕
while (i != mid + 1){
temp[k++] = arr[i++];
}
// 第二个元素没有遍历完毕
while (j != end + 1){
temp[k++] = arr[j++];
}
for (int n = start;n<=end;n++){
arr[n] = temp[n];
}
System.out.println("本次合并结果:"+Arrays.toString(arr));
return minSumResult;
}
逆序对和问题
题面
在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,请输出逆序对的数量
示例
[2,3,4,1,5,6]中逆序对的数量为3,分别为(2,1) (3,1) (4,1)
思路
即求每个数右边比其小的个数的和
代码实现
/**
* @Classname AntiOrder
* @Description 将归并的从小到大排序 换成 从大到小排序
*
*/
public class AntiOrder {
// 记录逆序对信息
private static Map<String,Object> antiOrderMap = new HashMap<>();
public static void main(String[] args) {
// 测试数据
int[] arr = {2,4,3,1,6,5};
antiOrder(arr);
}
/**
* 求逆序对
* @param arr
*/
public static void antiOrder(int[] arr){
if (arr == null || arr.length<2){
System.out.println("逆序对的数量为 0");
return;
}
int[] temp = new int[arr.length];
int minsum = mergeSort4AntiOrder(arr,temp,0,arr.length-1);
System.out.println("逆序对的数量为 "+minsum);
System.out.println("逆序对如下:");
System.out.println(antiOrderMap.toString());
antiOrderMap = new HashMap<>();
}
/**
* 求逆序对
* @param arr
* @param temp
* @param start
* @param end
* @return
*/
public static int mergeSort4AntiOrder(int[] arr,int[] temp,int start,int end){
if (start >= end){
return 0;
}
int mid = (start + end) / 2;
// 排序第一个数组 计算第一个数组中逆序对
int leftAnti = mergeSort4AntiOrder(arr,temp,start,mid);
// 排序第二个数组 计算第二个数组中逆序对
int rughtAnti = mergeSort4AntiOrder(arr,temp,mid+1,end);
// 合并两个有序数组 计算总的数组中逆序对
int allAnti = merge4AntiOrder(arr,temp,start,mid,end);
return leftAnti + rughtAnti +allAnti;
}
/**
* 求逆序对
* @param arr 存放两个有序列表的数组
* @param temp 临时数组
* @param start 左侧有序列表开始的下标
* @param mid 左侧结束位置下标 右侧开始位置下标:mid+1
* @param end 右侧有序列表结束下标
* @return 本组小和
*/
public static int merge4AntiOrder(int[] arr,int[] temp,int start,int mid,int end){
System.out.println("前一组开始下标 "+start);
System.out.println("前一组结束下标 "+mid);
System.out.println("后一组开始下标 "+(mid+1));
System.out.println("后一组结束下标 "+end);
int i = start; // i 用来记录 第一个数组的遍历位置
int j = mid + 1; // 用来记录第二个数组的遍历位置
int k = start; // 用来表示临时数组的下标
int antiResult = 0;
// i=mid+1:第一个数组先遍历完成
// j=end+1:第二个数组先遍历完成
while (i != mid +1 && j != end +1){
if (arr[i]<arr[j]){
temp[k++] = arr[j++];
}else {
// 统计逆序对
if (arr[i]>arr[j]){
antiResult += end-j+1; // 计算逆序对数量
// 逆序对内容
for (int index = j;index<=end;index++){
antiOrderMap.put(antiOrderMap.size()==0?"1":(antiOrderMap.size()+1)+"","("+arr[i]+","+arr[index]+")");
}
}
temp[k++] = arr[i++];
}
}
// 第一个数组没有遍历完毕
while (i != mid + 1){
temp[k++] = arr[i++];
}
// 第二个元素没有遍历完毕
while (j != end + 1){
temp[k++] = arr[j++];
}
for (int n = start;n<=end;n++){
arr[n] = temp[n];
}
System.out.println("本次合并结果:"+ Arrays.toString(arr));
return antiResult;
}
}
运行结果
瑞士轮问题
题面
在双⼈对决的竞技性⽐赛,如乒乓球、⽻⽑球、国际象棋中,最常⻅的赛制是淘汰赛和循环赛。前者
的特点是⽐赛场数少,每场都紧张刺激,但偶然性较⾼。后者的特点是较为公平,偶然性较低,但⽐
赛过程往往⼗分冗⻓。
本题中介绍的瑞⼠轮赛制,因最早使⽤于1895年在瑞⼠举办的国际象棋⽐赛⽽得名。它可以看作是淘
汰赛与循环赛的折衷,既保证了⽐赛的稳定性,⼜能使赛程不⾄于过⻓。
2*N 名编号为 1~2N 的选⼿共进⾏ R 轮⽐赛。每轮⽐赛开始前,以及所有⽐赛结束后,都会对选⼿
进⾏⼀次排名。排名的依据是选⼿的总分。选⼿的总分为第⼀轮开始前的初始分数加上已参加过的所
有⽐赛的得分和。总分相同的,约定编号较⼩的选⼿排名靠前。
每轮⽐赛的对阵安排与该轮⽐赛开始前的排名有关:第1名和第2名、第3名和第4名、……、第 2K-1
名和第 2K 名、…… 、第 2N-1 名和第 2N 名,各进⾏⼀场⽐赛。每场⽐赛胜者得1分,负者得0
分。也就是说除了⾸轮以外,其它轮⽐赛的安排均不能事先确定,⽽是要取决于选⼿在之前⽐赛中的
表现。
现给定每个选⼿的初始分数及其实⼒值,试计算在 R 轮⽐赛过后,排名第 Q 的选⼿编号是多少。我们
假设选⼿的实⼒值两两不同,且每场⽐赛中实⼒值较⾼的总能获胜。
示例
根据下表3名选⼿实⼒值及初始分数,求取4轮对阵后排名第2的选⼿为:编号1选⼿
思路
⾸先根据初始分数排序,然后在每次对阵结束后分为胜者组和败者组,分组时保证先进⾏对阵的选⼿
在前,后对阵的选⼿在后,这样既保证了胜者组和败者组两组先天有序,再进⾏归并排序的merge操
作,准备下次对阵。时间复杂度:O(N * logN + R * N)
快速排序
定义
基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分分别进行快速排序,整个排序过程可以递归进行,以达到整个数据变成有序序列
稳定性
不稳定
时间复杂度
O(N * logN)
代码
测试用例
int arr[] = {2,4,3,1,6,5};
原代码
/**
* 快速排序
* @param arr
* @param len
*/
public static void quickSort(int[] arr, int len){
System.out.println("初始数据 " + Arrays.toString(arr) + "\n\n");
quickSort(arr,0,len-1);
}
/**
* 快速排序
* @param arr
* @param low
* @param high
*/
public static void quickSort(int[] arr,int low,int high) {
int mid;
if (low<high){
mid = partition(arr,low,high); // 进行分割操作并返回基准元素
quickSort(arr,low,mid -1); // 右区间递归快速排序
quickSort(arr,mid+1,high); // 左区间递归快速排序
}
}
/**
* 返回基准
* @param arr
* @param low
* @param high
* @return
*/
public static int partition(int[] arr,int low, int high){
System.out.println("开始下标 "+ low);
System.out.println("结束下标 "+ high);
int i = low; // 待处理第一个下标
int j = high; // 待处理最后一个下标
// 分界值
int pivot = arr[low];
System.out.println("分界值 [" + pivot + "]");
while (i<j){
// 从右往左扫描 跳出循环:j==i 或 arr[j] <= arr[pivot]
while (i<j && arr[j]>pivot){
j--;
}
// arr[i] 和 arr[j] 交换后,i右移一位
if (i<j){
swap(arr,i,j);
System.out.println("右侧遍历 本次交换结果"+ Arrays.toString(arr));
i++;
}
// 从左往右扫描 跳出循环 i==j 或 arr[i]>pivot
while (i<j && arr[i]<=pivot){
i++;
}
// arr[i] 和 arr[j] 交换后,j左移一位
if (i<j){
swap(arr,i,j);
System.out.println("左侧遍历 本次交换结果" + Arrays.toString(arr));
j--;
}
}
// 返回最终划分完成后基准匀速所在的位置 i==j
System.out.println("新基准下标 " + i + "\n");
return i;
}
代码解释
第一次对整体进行遍历,以第一个作为基准,分为两部分
具体遍历过程:
j:先从右往左,找到小于等于基准的数之后,跳出循环
交换i j 所指向的元素,i++;此时arr[i]待判断
i:从左往右,找到大于基准的数之后,跳出循环
交换i j 所指向的值,j–;此时arr[j]待判断; arr[i]已判断
结束条件:i==j (本轮遍历结束)
每次遍历的基准都是arr[low];
运行结果
交换函数
/**
* 交换函数
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
扩展 应用
分类问题
题面
给定⼀个数组arr,和⼀个数num,请把⼩于等于num的数放在数组的左边,⼤于num的数放在数组的
右边。
思路
注:指定num不一定在数组中
指针p从-1位置开始,规划p左侧为⼩于num的数的区域,然后准备⼀个指针q指向下标0位置,如果q
指向元素⼩于等于num,则q指向元素与p+1指向元素交换,p、q往后移动;如果q指向元素⼤于
num,则q往后移动,直到q移出数组。
时间复杂度
O(N)
代码实现
/**
* @Classname Classify
* @Description 分类问题
*/
public class Classify {
public static void main(String[] args) {
// 测试数据
int arr[] = {2,1,1,1,4,8,1,6,5};
partition(arr,3);
}
/**
* 分类
* @param arr
* @param pivot 分界值
* @return
*/
public static int partition(int[] arr,int pivot){
System.out.println("分界值 [" + pivot + "]");
System.out.println("初始数据 " + Arrays.toString(arr)+"\n");
int i = -1; // 用于记录比pivot小的值的下标 -1表示不存在
int j = 0; // 用于遍历
for (;j<arr.length;j++){
// 只要比pivot小就和arr[i]交换
if (j<arr.length && arr[j]<=pivot){
swap(arr,i+1,j);
i++;
System.out.println("第"+(i+1)+"次交换结果 " +Arrays.toString(arr));
}
}
System.out.println("最终结果 " + Arrays.toString(arr)+"\n");
// 以下代码用于打印结果
System.out.println("前"+(i+1)+"个为小于等于"+pivot+"的元素");
System.out.println(pivot+"之前:");
System.out.print("[ ");
for (int n =0;n<=i;n++){
System.out.print(arr[n] + " ");
}
System.out.print("]");
System.out.println("\n"+pivot+"之后:");
System.out.print("[ ");
for (int n =i+1;n<arr.length;n++){
System.out.print(arr[n] + " ");
}
System.out.println("]");
return i;
}
/**
* 交换函数
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
运行结果
荷兰国旗问题
题面
给定⼀个数组arr,和⼀个数num,请把⼩于num的数放在数组的左边,等于num的数放在数组的中
间,⼤于num的数放在数组的右边。
时间复杂度
O(N)
代码实现
/**
* @Classname HollandFlag
* @Description 荷兰国旗
*/
public class HollandFlag {
public static void main(String[] args) {
// 测试数据
int arr[] = {-1,-2,-3,2,1,1,1,4,8,1,6,5};
partition(arr,arr.length,1);
}
/**
*
* @param arr
* @param len
* @param pivot
* @return
*/
public static int partition(int[] arr,int len,int pivot){
System.out.println("分界值 [" + pivot + "]");
System.out.println("初始数据 " + Arrays.toString(arr)+"\n");
int less = -1; // 记录比pivot小的值的下标 -1 表示没有
int more = len; // 记录比pivot大的值的下标 arr.length表示没有
System.out.println("分界值 [" + pivot + "]");
int i=0;
while (i<len&&less<more){
if (arr[i]<pivot){
swap(arr,i++,++less);
System.out.println("小于"+Arrays.toString(arr));
}else if (arr[i]>pivot&&i<more){// 因为是从左向右遍历
// 所以交换过来的数据(arr[i],原arr[--more])并没有被判断,所以这里i不加一
// 且要保证more和i的大小关系
swap(arr,i,--more);
System.out.println("大于"+Arrays.toString(arr));
}else {
i++;
}
}
System.out.println("最终结果 " + Arrays.toString(arr)+"\n");
// 以下代码用于打印结果
System.out.println("小于"+pivot);
System.out.print("[ ");
for (int n =0;n<=less;n++){
System.out.print(arr[n] + " ");
}
System.out.print("]");
System.out.println("\n等于"+pivot);
System.out.print("[ ");
for (int n =less+1;n<more;n++){
System.out.print(arr[n] + " ");
}
System.out.print("]");
System.out.println("\n大于"+pivot);
System.out.print("[ ");
for (int n =more;n<len;n++){
System.out.print(arr[n] + " ");
}
System.out.print("]");
return i;
}
/**
* 交换函数
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
运行结果
堆排序
定义
堆排序是利用堆这种数据结构所设计的一种排序算法。
堆是一种近似完全二叉树的结构,并同时满足堆积的性质:子结点的键值或索引总是小于(或者总是大于)它的父结点
子结点的键值或索引值总是小于其父结点:大根堆
子结点的键值或索引值总是大于其父结点:小根堆
稳定性
不稳定
时间复杂度
O(N*logN)
建堆方法
HeapInsert 上浮
描述
假定事先不知道有多少个元素,通过不断往堆里面插入元素进行调整来构建堆
步骤
- 增加堆的长度,在最末尾的地方加入最新插入的元素
- 比较当前元素和它的父结点值,如果比父结点值大,则交换两个元素,否则返回
- 重复2
时间复杂度
O(N * logN)
示例
-
给定⽆序序列,通过 HeapInsert 建⽴⼤根堆。
-
插⼊节点[6],与⽗节点[3]进⾏⽐较,并交换。
-
插⼊节点[8],与⽗节点[6]进⾏⽐较,并交换。
-
插⼊节点[5],与⽗节点[3]进⾏⽐较,并交换。
-
插⼊节点[9],与⽗节点[5]进⾏⽐较,并交换。
-
节点[9]继续与⽗节点[8]进⾏⽐较,并交换。
-
所有节点插⼊,建堆完成。
Heapify 下沉
从最后一个非叶子结点一直到根结点进行堆化的调整。
以大根堆为例,如果当前结点小于某个自己的孩子结点,那么当前结点和这个孩子交换,并持续往下递归调整
时间复杂度
O(N)
第一个非叶子结点
如果根结点在数组中的索引为0,那么最后 一个非叶子结点计算公式lastNonLeaf = (arr.length - 2)/2
设最后一个非叶子结点的位置为x,则最后一个叶子结点一定是2x + 1
或 2x + 2
示例
- 给定⽆序序列,通过 Heapify 建⽴⼤根堆。
- 从最后⼀个⾮叶⼦节点[6]开始调整(arr.length/2-1),与其孩⼦节点中的较⼤节点⽐较,并交换。
- 调整节点[5],与其孩⼦节点中的较⼤节点⽐较,并交换。
- 继续调整节点[5],与其新孩⼦节点中的较⼤节点⽐较,并交换。
- 所有⾮叶⼦节点调整完成,建堆完成。
代码
测试用例
int arr[] = {3,6,8,5,9};
原代码
/**
* @Classname HeapSort
* @Description 堆排序
* 时间复杂度 O(N * logN)
*/
public class HeapSort {
public static void main(String[] args) {
// 测试数据
int arr[] = {3,6,8,5,9};
//测试
maxHeapSort(arr,arr.length);
}
/**
* 堆排序
* @param arr
* @param len
*/
public static void maxHeapSort(int[] arr,int len){
System.out.println("初始数据:"+Arrays.toString(arr));
// 最后一个非叶子结点(len-2)/2 最后一个结点:len-1
for(int i = len/2 - 1; i>=0; i--){
System.out.println("父结点下标:"+i);
maxHeapify(arr,i,len - 1);
}
// 建堆完毕后结果输出
System.out.println("建堆完毕后结果:"+Arrays.toString(arr)+"\n");
// 先将第一个元素和已排好的元素前一位做交换(将最大的元素放到后面,类似于直接插入排序),再重新调整,直到排序完毕
for (int i = len -1;i>0;i--){
System.out.println("本次排序范围:0~"+(i-1));
swap(arr,0,i);
maxHeapify(arr,0,i-1);
}
System.out.println("最终结果:"+Arrays.toString(arr));
}
/**
* 建堆
* @param arr
* @param start
* @param end
*/
public static void maxHeapify(int arr[],int start,int end){
// 父结点和子结点下标
int dad = start;
int son = dad * 2 +1;
while(son <= end){ // 子结点在范围内
if (son + 1 <= end && arr[son] < arr[son +1]){
son++;
}
if (arr[dad]>arr[son]){ // 父结点大于子节点 跳出函数
return;
} else { // 否则交换父结点,并继续子结点和孙结点比较
swap(arr,dad,son);
dad = son;
son = dad * 2 +1;
}
System.out.println("本次交换结果:"+Arrays.toString(arr));
}
}
/**
* 交换函数
* @param arr
* @param i
* @param j
*/
public static void swap(int[] arr,int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
运行结果
步骤
将堆顶结点与末尾结点交换,使末尾结点最大。然后继续调整堆,再将堆顶结点与末尾结点交换,得到第二大结点。如此反复进行交换 重建
- 已有⼤根堆,对其进⾏堆排序。
- 将堆顶节点[9]与末尾节点[5]进⾏交换,交换完成后将节点[9]移出⼆叉树,并对节点[5]进⾏Heapify调
整。
- 将堆顶节点[8]与末尾节点[3]进⾏交换,交换完成后将节点[8]移出⼆叉树,并对节点[3]进⾏Heapify调
整
- 将堆顶节点[6]与末尾节点[5]进⾏交换,交换完成后将节点[6]移出⼆叉树,并对节点[5]进⾏Heapify调
整。
- 将堆顶节点[5]与末尾节点[3]进⾏交换,交换完成后将节点[5]移出⼆叉树,此时只剩⼀个节点,堆排序
结束。
计数排序
定义
计数排序是一种特殊的桶排序
计数排序是一种非比较性质的排序算法,元素从未排序状态变为已排序状态的过程,是由额外控件的辅助和元素本身的值决定的。
计数排序过程中不存在元素之间的比较和交换操作。根据元素本身的值,将每个元素出现的次数记录到辅助空间后,通过对辅助空间内数据的计算,即可确定每一个元素最终的位置
稳定性
稳定
时间复杂度
O(N+K) K为数值范围
适用场合
计数排序需要占用大量空间,仅适用于数据比较集中的情况.
步骤
- 根据待排序集合中最大元素和最小元素的差值范围,申请额外空间
- 遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内
- 对额外空间内数据进行计算,得出每一个元素的位置
- 将待排集合每一个元素移动到计算得出的正确位置上
代码
测试用例
int arr[] = {37,18,21,21,49,0,25,6,14};
原代码
/**
* 计数排序
* @param arr
* @param len
*/
public static void countSort1(int[] arr,int len){
// 求取最大值最小值,确定桶区间范围
// 记录数组中的最大值
int max = Integer.MIN_VALUE;
// 记录数组中的最小值
int min = Integer.MAX_VALUE;
for (int i = 0;i<len;i++){
max = Math.max(max,arr[i]);
min = Math.min(min,arr[i]);
}
System.out.println("最大值:"+max);
System.out.println("最小值:"+min);
// 长度为max的话,min如果是负数或者0的时候,下面的help[mapPos]会越界
// int help[] = new int[max];
// 辅助数组
int help[] = new int[max-min+1];
// 找出每个数字出现的次数
for (int i = 0;i<len;i++){
int mapPos = arr[i] - min;
help[mapPos]++;
}
int index = 0;
for (int i = 0;i<help.length;i++){
while (help[i]-->0){
arr[index++] = i+min;
}
}
System.out.println("排序结果:"+ Arrays.toString(arr));
}
运行结果
桶排序(箱排序)
定义
桶排序的工作原理是将数组分到有限数量的桶中,每个桶再个别排序
有可能使用别的排序算法或者以递归方式继续使用桶排序进行排序
稳定性
稳定
时间复杂度
平均时间复杂度:O(N + C)
C = N * (logN-logM)
N为待排数据,M为桶数量
当N=M时,最好效率达到O(N)
适用场合
两个必要条件:
- 最大最小值相差较大
- 数据分布均匀,否则可能将数据集中到同一个桶中,便失去了桶排序的意义
过程分析
桶排序的基本思想:把数组arr划分为n个大小相同的子区间(桶),每个子区间各自排序,最后合并。
计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况
-
找出待排序数组的最大值和最小值
-
使用动态数组ArrayList作为桶,桶中放置的元素也使用ArrayList存储。
桶的数量:(max-min)/arr.length +1
-
每个桶各自排序
-
遍历桶数组,把排好的元素放进输出数组
代码
测试用例
int arr[] = {37,18,21,21,49,0,25,6,14};
原代码
/**
* 桶排序
* @param arr
* @param len
*/
public static void bucketSort(int[] arr,int len){
// 求取最大值最小值,确定桶区间范围
// 记录数组中的最大值
int max = Integer.MIN_VALUE;
// 记录数组中的最小值
int min = Integer.MAX_VALUE;
for (int i = 0;i<len;i++){
max = Math.max(max,arr[i]);
min = Math.min(min,arr[i]);
}
System.out.println("最大值:"+max);
System.out.println("最小值:"+min);
// 桶数
int bucketNum = (max - min) / arr.length + 1;
System.out.println("桶数 " + bucketNum);
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0;i<bucketNum;i++){
bucketArr.add(new ArrayList<Integer>());
}
// 把每个元素放入桶
for (int i = 0;i<len;i++){
int num = (arr[i] - min) / len;
System.out.println("num:"+arr[i]+" index of num :" + num);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for (int i = 0;i<bucketArr.size();i++){
Collections.sort(bucketArr.get(i));
}
// 输出排序后的桶数据
System.out.println(bucketArr.toString());
//合成新数组
int i = 0;
for (ArrayList<Integer> arrItem:bucketArr){
if (arrItem.size()!=0){
for (int item:arrItem){
arr[i++]=item;
}
}
}
System.out.println(Arrays.toString(arr));
}
运行结果
基数排序
定义
基数排序属于分配式排序,它是透过键值的部分咨询,将要排序的元素分配至某些”桶“中,藉以达到排序的作用
稳定性
稳定
时间复杂度
O(D * N)(D为基数位数)
代码
测试用例
int[] arr = {37,18,21,21,49,0,25,6,14};
原代码
private static void radixSort(int[] arr) {
//待排序列最大值
int max = arr[0];
int exp;//指数
//计算最大值
for (int anArr : arr) {
if (anArr > max) {
max = anArr;
}
}
//从个位开始,对数组进行排序
for (exp = 1; max / exp > 0; exp *= 10) {
//存储待排元素的临时数组
int[] temp = new int[arr.length];
//分桶个数
int[] buckets = new int[10];
//将数据出现的次数存储在buckets中
for (int value : arr) {
//(value / exp) % 10 :value的最底位(个位)
buckets[(value / exp) % 10]++;
}
//更改buckets[i],
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
//将数据存储到临时数组temp中
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
//将有序元素temp赋给arr
System.arraycopy(temp, 0, arr, 0, arr.length);
}
}
代码缺陷
目前不支持非负数排序
改进思路
一种改进如下:
- 遍历数据的时候找出数组的最小值
- 如果小于0,则将数组中的所有数据加上这个负数的相反数
- 排好之后,遍历减去这个相反数,得到原数据的排序结果
to be continued …