目录
一.排序
1.1排序的概念
排序:就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若进过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]前面,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断在内外存之间移动数据的排序。
1.2排序的应用场景
- 数据分析和统计:在处理大量数据时,排序可以帮助快速找到最大值、最小值、中位数、分位数等统计信息。
- 数据库查询优化:数据库系统经常使用排序算法来加速查询,比如索引的构建和查询时的排序操作。
- 搜索引擎:搜索引擎的索引构建过程中,需要对网页进行排序以提高检索效率。
- 图形渲染:在计算机图形学中,物体的渲染顺序可能会影响最终的视觉效果,例如深度排序可以避免遮挡问题。
- 并行和分布式计算:在分布式系统中,排序可以用于聚合操作,如分布式排序算法(如MapReduce中的Sort阶段)。
- 机器学习和数据挖掘:在训练模型时,可能需要对特征进行排序以进行特征选择或降维。
- 资源调度:操作系统中的进程调度、任务分配等,可能需要用到优先级队列,这往往依赖于排序。
- 算法竞赛和面试:在编程竞赛和面试中,手写排序算法是常见的考核点,考察候选人的算法基础和解决问题的能力。
- 文件系统:在文件系统的目录和文件列表中,排序可以方便用户浏览和查找。
- 计算机网络:在网络协议中,有时需要按照特定顺序处理数据包,例如TCP的排序。
不同的排序算法有各自的优缺点,适用于不同的场景。
二.插入排序
2.1直接插入排序
2.1.1基本思想
直接插入排序是一种简单的插入排序法,其基本思想是把待排序的激励按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
我们在玩扑克牌的时候,就是用到了插入排序的思想。
2.1.2基本步骤
- 从第二个元素开始,依次与前面的元素比较。(默认第一个元素是排好的)
- 如果当前元素小于前面的元素,则将前面的元素往后移动一位,为当前的元素插入腾出位置。
- 重复这个过程,直到找到合适的位置插入元素。
- 继续处理下一个未排序的元素,直到所有元素都排序完成。
动图演示:
2.3.1.算法分析
时间复杂度:插入排序的时间复杂度取决于待排序序列的初始状态。
- 在最好情况下(序列已经是排好序),时间复杂度为O(n)。
- 在最坏情况下(序列是逆序),时间复杂度是O(n^2)。
- 平均时间复杂度为O(n^2).
空间复杂度:由于插入排序是原地排序,不需要额外的存储空间,所以空间复杂度为O(1)。
稳定性:插入排序是稳定的排序算法。(相同元素在排完序,其相对位置不变)
适用场景:对于规模较小或大部分数据已经排好序的数据有较好的效率。
2.1.4代码实现
1.java版
public static void InsertSort(int[] arr){
// 默认第一个元素是排好的
for (int i = 1; i < arr.length; i++) {
// 将当前元素暂存,以便比较和移动
int temp=arr[i];
int j=i-1;
// 比较当前元素和已排序部分的元素,将大于当前元素的元素后移
for(;j>=0;j--){
if(arr[j]>temp){
arr[j+1]=arr[j];
}else{
break; // 当找到合适的位置时,跳出循环
}
}
// 将当前元素插入到正确的位置
arr[j+1]=temp;
}
}
2.c语言版
#include<stdio.h>
void inser_sort(int arr[],int len) {
//默认第一个元素是排好的
//从第二个元素开始排序
for (int i = 1; i < len; i++)
{
int temp = arr[i];
int j = i - 1;
for (; j >= 0; j--)
{
if (arr[j] > temp) {//如果j位置的元素比temp大,那么就将j位置的元素往后移动
arr[j + 1] = arr[j];
}
else {//如果temp处的值比arr[j]的元素大,停止循环,进行插入
break;
}
}
arr[j + 1] = temp;
}
}
void print(int arr[],int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 5,4,8,9,6,3,10 };
int len = sizeof(arr) / sizeof(arr[0]);//求元素个数
printf("排序前:");
print(arr, len);
inser_sort(arr, len);
printf("排序后:");
print(arr,len);
return 0;
}
2.2希尔排序
2.2.1基本思想
希尔排序又称为缩小增量法,是一种改进的插入排序。其基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。
2.2.2基本步骤
- 选择增量gap:选择一个增量序列,我们选比较常用的(gap=gap/2)
- 分组:按照增量,将数组划分为若干个子序列。
- 进行插入排序:对每一个子序列都进行插入排序操作,子序列中每个元素间隔为gap。
- 缩小增量:gap=gap/2,重复上述操作。
- 最终排序:当gap缩小到1时,即是对整个序列来一次直接插入排序。
2.2.3算法分析
时间复杂度:希尔排序的时间时间复杂度取决于所选的间隔序列。
- 最好情况下(选择有效的间隔序列,如Hibbard序列、Sedfewick序列或Hoare序列):时间复杂度可以达到O(n^(3/2))。
- 最坏情况下(选择不恰当的间隔序列):时间复杂度接近O(n^2).
- 平均时间复杂度:介于O(n^1.3)~O(n^1.5)之间
- 空间复杂度:O(1),原地排序,不需要额外的空间。
- 稳定性:不稳定。(在排序过程中可能会改变相等元素的相对顺序)
- 适用场景:希尔排序在处理大型数据集时速度更快,尤其是当数据近乎有序时。
总结:
- 希尔排序是对直接插入排序的优化
- 当gap>1时都是预排序,目的是为了让数组更接近于有序,当gap==1时,数组已经接近有序,这样效率就会提高,可以达到优化的效果。
- 希尔排序的时间复杂度不固定,由于gap的取值方法有很多,因此时间复杂度也不同
2.2.4代码实现
1.java版
/**
* 实现Shell排序算法。
* @param arr 待排序的整型数组。
* @param gap 初始间隔,用于控制排序过程中的比较与交换频率。
*/
public static void shellSort(int[] arr,int gap){
// 遍历数组,从gap位置开始,逐个元素进行插入排序
for(int i=gap;i<arr.length;i++){
// 保存当前要排序的元素
int temp=arr[i];
int j=i-gap;
// 退一步比较,如果当前元素小于已排序的部分,则继续后移
for(;j>=0;j-=gap){
if(arr[j]>temp){
arr[j+gap]=arr[j];
}else{
// 找到合适的位置,插入当前元素
break;
}
}
arr[j+gap]=temp;
// 打印每一步的排序状态,仅作演示用途
System.out.println(Arrays.toString(arr));
}
}
/**
* 对给定的整型数组进行Shell排序。Shell排序是一种改进的插入排序算法,通过设置初始间隔(gap)来排序,
* 之后逐渐减小间隔,直到间隔为1进行标准插入排序,从而减少排序的次数,提高效率。
*
* @param arr 待排序的整型数组。
*/
public static void shell(int[] arr){
// 初始化间隔为数组长度
int gap=arr.length;
// 当间隔大于1时进行循环
while(gap>1){
// 根据公式逐渐减小间隔
gap=gap/2;
// 使用shellSort方法对数组根据当前间隔进行排序
shellSort(arr,gap);
}
}
public static void main(String[] args) {
int arr[]={51,5,8,4,56,1,21,5,43};
shell(arr);
System.out.println(Arrays.toString(arr));
}
2.c语言版
#include<stdio.h>
void shellsort(int arr[], int gap, int len)
{
for (int i = gap; i < len; i++)
{
int temp = arr[i];
int j = i - gap;
for (; j >= 0; j-=gap)
{
if (arr[j] > temp) {
arr[j + gap] = arr[j];
}
else
{
break;
}
}
arr[j + gap] = temp;
}
}
void shell(int arr[],int len)
{
int gap = len;
while (gap > 1)
{
gap = gap / 3 + 1;
shellsort(arr, gap,len);
}
}
void print(int arr[],int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 5,4,8,9,6,3,10 };
int len = sizeof(arr) / sizeof(arr[0]);//求元素个数
printf("排序前:");
print(arr, len);
shell(arr,len);
printf("排序后:");
print(arr,len);
return 0;
}
三.选择排序
3.1基本思想
选择排序是一种简单直观的排序算法,任何数据排序都是O(n^2),基本思想:在未排序的序列找到最小(或最大)元素,存放到序列的起始位置,接着,在从未排序序列中找到最小(或最大)元素,放到已经排好序的序列末尾,以此类推。
3.2基本步骤
- 在为排序序列中找最小(大)元素,存放到排序序列的其实位置
- 再从剩余的未排序序列中继续找最小(大)元素,放的己排序序列的末尾
- 重复上述过程,直到序列有序。
动图演示:
3.3算法分析
时间复杂度:无论数组顺序如何,时间复杂度都为O(n^2)
空间复杂度:O(1),选择排序也是原地排序,不需要额外的存储空间
稳定性:不稳定。(排序过程中可能会改变相等元素的相对顺序)
适用场景:小规模的数据或对性能要求不高的情况下都可以使用。
3.4代码实现
1.java版
/**
* 选择排序算法对整型数组进行排序。
* 该方法采用原地排序,即不需要额外的存储空间。
* 对数组进行从头到尾的遍历,每次选择未排序部分中的最小元素,
* 然后将其与未排序部分的第一个元素交换位置。
* 这样,每次交换后,未排序部分的第一个元素就是当前最小元素。
*
* @param array 待排序的整型数组。
* 注:该方法不返回任何值,排序是直接在传入的数组上进行。
*/
public static void selectSort(int[] array){
// 遍历整个数组,进行n次选择排序
for(int i=0;i<array.length;i++){
// 假设当前未排序部分的最小值所在位置为i
int min=i;
// 从i+1开始,查找未排序部分的最小值
for(int j=i+1;j< array.length;j++){
// 如果找到比当前最小值更小的元素,则更新最小值位置
if(array[j]<array[min]){
min=j;
}
}
// 将最小值与当前位置(i)的元素交换,确保最小值被放到正确的位置
swap(array,i,min);
}
}
/**
* 交换数组中两个位置的元素。
*
* @param array 需要进行交换的数组。
* @param i 第一个位置的索引。
* @param j 第二个位置的索引。
*/
private static void swap(int[] array, int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
双指针法
/**
* 选择排序算法的实现,对给定的整型数组进行升序排序。
*
* @param array 需要排序的整型数组。
*/
public static void selectSort1(int[] array){
// 初始化左右指针,分别指向数组的起始和末尾
int left=0;
int right=array.length-1;
// 当左指针小于右指针时,持续进行排序操作
while(left<right){
// 初始化最小值索引和最大值索引为左指针位置
int minindex=left;
int maxindex=right;
// 遍历数组,寻找最小值和最大值的索引
for(int i=left+1;i<=right;i++){
if(array[minindex]>array[i]){
minindex=i;
}
if(array[maxindex]<array[i]){
maxindex=i;
}
}
// 将最小值与左指针位置的元素交换,确保左边部分已排序
swap(array,minindex,left);
// 如果最大值索引与左指针重合,则将最大值索引更新为最小值索引
if(maxindex==left){
maxindex=minindex;
}
// 将最大值与右指针位置的元素交换,确保右边部分已排序
swap(array,maxindex,right);
// 更新左右指针位置
left++;
right--;
}
}
/**
* 交换数组中两个位置的元素。
*
* @param array 需要进行交换的数组。
* @param i 第一个位置的索引。
* @param j 第二个位置的索引。
*/
private static void swap(int[] array, int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
2.c语言版
#include<stdio.h>
void swap(int arr[], int index1, int index2)
{
int temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
void selectsort(int arr[], int len)
{
for (int i = 0; i < len; i++)
{
int min = i;
for (int j = i + 1; j < len; j++)
{
if (arr[j] < arr[min])
{
min = j;
}
}
swap(arr, i, min);
}
}
int main()
{
int arr[] = { 5,4,8,9,6,3,10 };
int len = sizeof(arr) / sizeof(arr[0]);//求元素个数
printf("排序前:");
print(arr, len);
selectsort(arr, len);
printf("排序后:");
print(arr,len);
return 0;
}
四.冒泡排序
4.1基本思想
冒泡排序是一种简单的排序算法,基本思想:重复的遍历比较要排序的序列,比较相邻的两个元素,如果顺序错误就进行交换,直到全部交换完成。
4.2基本步骤
- 比较相邻的两个元素,如果第一个比第二个大,就进行交换。(以升序为例)
- 对每一对相邻的元素都进行上述操作,从第一对到最后一对元素,可以确定最后的元素是最大的。
- 针对所有元素,重复上述操作,除了最后一个元素
- 重复1~3,直到排完序
动图演示:
4.3算法分析
时间复杂度:
- 最好情况(已经排好序):O(n) (只需要遍历一遍数组确认是否有序即可)
- 最坏情况(逆序):O(n^2),需要进行n*(n-1)/2次比较
空间复杂度:O(1)。(原地排序,不需要额外的空间)
稳定性:稳定。
适用场景:小规模的数据
4.4代码实现
1.java版
/**
* 实现冒泡排序算法。
* 对给定的整型数组进行排序,使得数组中的元素从小到大排列。
*
* @param arr 需要排序的整型数组。
*/
public static void bubbleSort(int[] arr){
// 初始化标志位为false,用于判断数组是否已经排序完成
boolean flag=false;
// 外层循环控制排序的轮数,每轮确保一个最大元素被放置到正确的位置
for(int i=0;i<arr.length-1;i++){
// 内层循环控制每轮排序中两两比较的次数
for(int j=0;j<arr.length-i-1;j++){
// 如果当前元素大于下一个元素,则交换它们的位置
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
// 交换后标志位设为true,表示数组顺序未稳定,还需继续排序
flag=true;
}
}
// 如果一轮比较中没有发生交换,说明数组已经有序,提前结束循环
if(!flag){
break;
}
}
}
2.c语言版
#include<stdio.h>
#include<stdbool.h>
void bubble_sort(int arr[], int len)
{
bool flag = false;
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if (!flag)
{
break;
}
}
}
int main()
{
int arr[] = { 5,4,8,9,6,3,10 };
int len = sizeof(arr) / sizeof(arr[0]);//求元素个数
printf("排序前:");
print(arr, len);
bubble_sort(arr, len);
printf("排序后:");
print(arr,len);
return 0;
}
五.快速排序
5.1基本思想
快速排序用到了分治思想,同样用到分治思想的还有归并排序。基本思想是在序列中选择一个基准,将序列通过排序切分成两个子序列,一个子序列存放比基准小的元素,一个子序列存放比基准大的元素。分别对这两部分子序列重复上述操作,直到序列有序
5.2基本步骤
- 选基准(一般是序列中的第一个元素)
- 让数组进行排序,将数组切分成两个子数组,一个子数组存放比基准小的元素,一个子数组存放比基准大的元素。
- 重复1~2操作,直到有序
5.3算法分析
时间复杂度:
- 在最好的情况下:时间复杂度为O(n*logn)
- 在最坏情况下(已排序或逆序):O(n^2)
- 平均时间复杂度:O(n*logn)
空间复杂度:O(logn)
稳定性:不稳定。(相等的元素可能会改变它们的相对顺序)
适用场景:
- 大数据量排序:快速排序在处理大型数据集时表现出色,因为它的时间复杂度在平均情况下是线性对数级的,即O(n log n)。这使得它在处理大规模数据时比其他O(n^2)的排序算法(如冒泡排序、插入排序)更快。
- 内存有限:快速排序通常比其他高效的排序算法(如归并排序)更节省内存,因为它不需要额外的数据结构来存储中间结果。这使得它在内存受限的环境中更为合适。
- 原地排序:快速排序可以在原始数组上进行排序,不需要额外的存储空间,这被称为原地排序。对于某些应用,这种特性是必要的。
- 并行计算:快速排序的分治策略使得它容易并行化,因为每个子序列的排序可以独立进行。在多核处理器或分布式系统中,这种并行性可以显著提高排序速度。
5.4快速排序实现
5.4.1Hoare法
Hoare法:
- 选取第一个元素作为基准值(key),我们的目的是使基准左边的数都比基准小,基准右边的数都比基准大。
- 定义两个下标变量,一个变量走左边(L),一个变量走右边(R)。先让R开始往左走,当R遇到比基准值小的数就停下来,再让L往右走,找到比基准值大的数,让R和L位置的值互换,再让R走,重复上述操作。
- R停下来的情况有两种:1、遇到比基准值小的数 2、R和L相遇
- L停下来的情况也有两种:1、遇到比基准值大的数 2、R和L相遇
- 如果让L停下来的条件是1,那么就交换L和R位置的值,接着回到第二步,R继续往左移动,重复此过程,直到L和R相遇。
- 一旦L和R相遇,就说明第一层的数据变量结束,交换L和基准值并进入下一层的排序。
注意:为什么先走R,而不先走L?
先走R,可以保证在L和R相遇时一定会是比基准值小的数。这是因为R先移动,在找到比基准值小的数前是不会停止的;而L移动的前提条件是R找到了比基准值小的数(这一特性使R静止的位置一定会是比基准值key小的数)
代码实现:
class quicks {
public static void main(String[] args) {
int[] arr={8,5,2,4,4,6,15};
quick(arr);
System.out.println(Arrays.toString(arr));
}
public static void quick(int[] arr){
quickSort(arr,0,arr.length-1);
}
public static void swap(int[] arr,int left,int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
public static int partitionHoare(int[] arr,int left,int right){
//定义一个变量存放基准值
int leftindex=left;//保留左侧的位置,便于与基准值与left交换
int key=arr[left];
if(left>=right){
return left;
}
while(left<right){
while (left<right&&arr[right]>=key){//如果右侧的值大于等于key,则right--
right--;
}
//走到此处,说明右侧找到了比key小的值
while (left<right&&arr[left]<=key){//如果左侧的值小于等于key,则left++
left++;
}
//走到这,交换左侧的值和右侧的值
swap(arr,left,right);
}
//left和right相遇,则交换left和leftindex的值
swap(arr,leftindex,left);
return left;//返回left,作为数组的分界点
}
/**
* 使用Hoare分区的快速排序算法对数组进行排序。
*
* @param arr 待排序的整型数组。
* @param left 分区起始索引。
* @param right 分区结束索引。
* 注:该方法不直接返回任何值,而是通过递归对数组元素进行排序。
*/
public static void quickSort(int[] arr,int left,int right){
// 当左指针大于等于右指针时,表示区间内没有元素,或只有一个元素,无需排序,直接返回
if(left>=right){
return;
}
// 使用Hoare分区方法进行分区,并返回基准元素的位置
int par=partitionHoare(arr,left,right);
// 对分区点左边的子数组进行递归排序
quickSort(arr,left,par-1);
// 对分区点右边的子数组进行递归排序
quickSort(arr,par+1,right);
}
}
5.4.2挖坑法
我们在了解Hoare法后,来理解挖坑法也不难。
挖坑法的步骤:
- 选择基准,将基准挖出并存在key中,此时挖出基准的位置就相当于一个坑位
- 让R先走(此时L停止不动),每走一步都与基准值进行比较。
- 让R停下来的条件有两个:1、遇到比基准值小的数 2、L和R相遇。
- 如果让R停下来的条件是1,那么就将R位置处的值填到坑位中,那么此时R此处就有一个坑位
- 接着让L开始走(此时R停止不动),每走一步也是要与基准值进行比较
- 让L停下来的条件也有两个:1、遇到比基准值大的数 2、L和R相遇
- 如果让L停下来的条件是1,那么就将L位置处的值填到坑位中(此时坑位是在R),这时坑位的位置就是L
- 重复上述操作,直到R和L相遇,那么就将key值放到坑位置,第一趟排序就此完成。
public class quicks {
public static void main(String[] args) {
int[] arr={8,5,2,4,4,6,15};
quick(arr);
System.out.println(Arrays.toString(arr));
}
public static void quick(int[] arr){
quickSort(arr,0,arr.length-1);
}
public static void swap(int[] arr,int left,int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
/**
* 对数组进行分区操作,使得所有小于关键值的元素位于左侧,所有大于等于关键值的元素位于右侧。
* @param arr 待分区的数组
* @param left 分区的起始索引
* @param right 分区的结束索引
* @return 分区后关键值所在的索引
*/
public static int partitionHole(int[] arr, int left, int right) {
int key = arr[left]; // 以起始位置的元素作为关键值
while (left < right) {
// 从右侧找到第一个小于关键值的元素
while (left < right && arr[right] >= key) {
right--;
}
swap(arr, left, right); // 将找到的小于关键值的元素与起始位置的元素交换
// 从左侧找到第一个大于等于关键值的元素
while (left < right && arr[left] <= key) {
left++;
}
swap(arr, left, right); // 将找到的大于等于关键值的元素与上一步交换后的元素交换
}
// 将关键值放置到正确的位置上
arr[left] = key;
return left;
}
/**
* 使用Hoare分区的快速排序算法对数组进行排序。
*
* @param arr 待排序的整型数组。
* @param left 分区起始索引。
* @param right 分区结束索引。
* 注:该方法不直接返回任何值,而是通过递归对数组元素进行排序。
*/
public static void quickSort(int[] arr,int left,int right){
// 当左指针大于等于右指针时,表示区间内没有元素,或只有一个元素,无需排序,直接返回
if(left>=right){
return;
}
// 使用Hoare分区方法进行分区,并返回基准元素的位置
//int par=partitionHoare(arr,left,right);
int par=partitionHole(arr,left,right);
// 对分区点左边的子数组进行递归排序
quickSort(arr,left,par-1);
// 对分区点右边的子数组进行递归排序
quickSort(arr,par+1,right);
}
}
5.4.3双指针法
双指针法在理解的时候可能会有一点难。
基本思路:
- 选择一个基准值key
- 先有两个指针(cur,prev),prev此时在left位置,cur在left+1处
- 在走的时候,我们需要判断cur位置的值是否小于key:
1、如果小于等于key那就让prev++,并且判断prev++完之后的位置是否和cur重合,如果不是就进行交换。
2、如果大于key,那就让cur往后走,直到找到比key小的值,再进行交换。
- 结束条件:cur>right
- 结束之后,交换prev和key的值
在整个过程中,prev位置之前的数据都小于基准值key,而prev和cur之间的值都保证比基准值key要大。在prev和cur交换的过程中,相当于把大的数字往后甩,小的数字往前插入,在cur遍历到最后,比基准值小的数字也就成功插入到了前面,而比基准值大的数字也都被甩到后面。此时交换prev和key位置的值,就完成了 第一层排序。
/**
* 使用QuickSort算法中的分区方法对数组进行分区操作。
* 分区过程中,小于等于关键元素的值会被移到左侧,大于关键元素的值会被移到右侧。
* @param arr 要进行分区操作的整型数组
* @param left 分区的起始索引
* @param right 分区的结束索引
* @return 分区后关键元素的最终位置的索引
*/
public static int partitionQL(int[] arr,int left,int right){
// 将第一个元素作为关键元素
int key=arr[left];
int index=left;
int prev=left;
int cur=left+1;
// 从第二个元素开始遍历数组
while(cur<=right){
// 如果当前元素小于等于关键元素,并且当前位置之前的元素不等于当前元素
// 则交换当前元素与之前位置的元素,保证有序性
if(arr[cur]<=key&&arr[++prev]!=arr[cur]){
swap(arr,prev,cur);
}
cur++;
}
// 将关键元素交换到正确的位置上
swap(arr,prev,index);
return prev;
}
5.5 快速排序的优化
快速排序是一个高效的排序算法,但是其不适合在已经排序或基本排序的序列中使用,会导致其时间复杂度退化到O(n^2).对于规模非常大的数据,快速排序也不适合使用,递归深度过大,会导致栈溢出。
为了克服这些缺点,我们可以对快排进行优化,常见的优化方法有:随机选key、三数取中法、非递归实现
5.5.1随机选key
随机化选择枢轴是快速排序算法中提高效率和稳定性的关键步骤之一,尤其对于大型和不可预知的数据集来说,这一优化至关重要。(right - left + 1) 是为了控制范围防止越界
int rands=(int)Math.random();
rands%=(right-left+1);
rands+=left;
swap(arr,left,rands);
以Hoare法为例
/**
* 使用Hoare分区法进行数组分区。
*
* @param arr 待分区的整型数组。
* @param left 分区的起始索引。
* @param right 分区的结束索引。
* @return 返回分区点的索引。
* 这个方法采用了Hoare分区法来对数组进行分区操作,主要用于快速排序算法中。
* 它的选择基准值的方法是随机的,这有助于提高分区操作的效率,
* 通过交换数组元素,将小于等于基准值的元素放在基准值的左侧,大于基准值的元素放在右侧。
*/
public static int partitionHoare(int[] arr,int left,int right){
// 初始化基准值和左指针
int leftindex=left; // 用于存放基准值最终位置的指针
if(left>=right){
return left;
}
// 随机选择一个基准值位置,以提高分区效率
int rands=(int)Math.random();
rands%=(right-left+1);
rands+=left;
swap(arr,left,rands); // 将基准值与随机位置元素交换
int key=arr[left]; // 以左端点元素作为基准值
// 使用双指针法进行分区
while(left<right){
// 右指针向左寻找小于等于基准值的元素
while (left<right&&arr[right]>=key){
right--;
}
// 左指针向右寻找大于基准值的元素
while (left<right&&arr[left]<=key){
left++;
}
// 交换找到的左右元素,使小于基准值的元素集中于左侧,大于基准值的集中于右侧
swap(arr,left,right);
}
// 将基准值放置到正确的位置上
swap(arr,leftindex,left);
return left; // 返回基准值最终位置作为分区点
}
5.5.2三数取中法
三数取中法的基本思想是取序列的第一个元素、最后一个元素和中间元素的中间值作为基准值。
这样做的好处是,如果输入数据已经部分有序,或者输入数据的分布有偏斜,这种方法通常可以避免选择极端值作为基准,从而得到一个更好的分割效果。这会使得分区更均衡,加快排序的速度,尤其是在处理大型数据集时。
//取中位
public static int midGet(int[] array,int left,int right){
int mid=(right+left)/2;
//第一种情况left大于right,有三种
//mid left right
//left right mid
//left mid right
if(array[left]<array[right]){
if(array[left]>array[mid]){
return left;
}else if(array[right]<array[mid]){
return right;
}else{
return mid;
}
}else{
//第二种情况left小于right
//right left mid
//mid right left
//right mid left
if(array[left]<array[mid]){
return left;
}else if(array[mid]<array[right]){
return right;
}else{
return mid;
}
}
}
以Hoare为例
public static void quickSort(int[] arr,int left,int right){
// 当左指针大于等于右指针时,表示区间内没有元素,或只有一个元素,无需排序,直接返回
if(left>=right){
return;
}
int mid=midTGet(arr,left,right);
swap(arr,left,mid);
// 使用Hoare分区方法进行分区,并返回基准元素的位置
int par=partitionHoare(arr,left,right);
// 对分区点左边的子数组进行递归排序
quickSort(arr,left,par-1);
// 对分区点右边的子数组进行递归排序
quickSort(arr,par+1,right);
}
public static int partitionHoare(int[] arr,int left,int right){
// 初始化基准值和左指针
int leftindex=left; // 用于存放基准值最终位置的指针
if(left>=right){
return left;
}
// 随机选择一个基准值位置,以提高分区效率
int rands=(int)Math.random();
rands%=(right-left+1);
rands+=left;
swap(arr,left,rands); // 将基准值与随机位置元素交换
int key=arr[left]; // 以左端点元素作为基准值
// 使用双指针法进行分区
while(left<right){
// 右指针向左寻找小于等于基准值的元素
while (left<right&&arr[right]>=key){
right--;
}
// 左指针向右寻找大于基准值的元素
while (left<right&&arr[left]<=key){
left++;
}
// 交换找到的左右元素,使小于基准值的元素集中于左侧,大于基准值的集中于右侧
swap(arr,left,right);
}
// 将基准值放置到正确的位置上
swap(arr,leftindex,left);
return left; // 返回基准值最终位置作为分区点
}
//取中位
public static int midTGet(int[] array,int left,int right){
int mid=(right+left)/2;
//第一种情况left大于right,有三种
//mid left right
//left right mid
//left mid right
if(array[left]<array[right]){
if(array[left]>array[mid]){
return left;
}else if(array[right]<array[mid]){
return right;
}else{
return mid;
}
}else{
//第二种情况left小于right
//right left mid
//mid right left
//right mid left
if(array[left]<array[mid]){
return left;
}else if(array[mid]<array[right]){
return right;
}else{
return mid;
}
}
}
public static void quick(int[] arr){
quickSort(arr,0,arr.length-1);
}
public static void swap(int[] arr,int left,int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
5.5.3小区间优化
你是否考虑过这样一个问题,当快排递归到最后几层时,会产生多少小区间。
我们可以看到,当到最后几层时,如果进行递归,至少占全部递归次数的80%以上。
为了减少消耗,我们待排序数字小于10的时候,就可以用直接插入排序。
public static void quickSort(int[] arr,int left,int right){
// 当左指针大于等于右指针时,表示区间内没有元素,或只有一个元素,无需排序,直接返回
if(left>=right){
return;
}
if(right-left<=10){
InsertSort(arr,left,right);
return;
}
int mid=midTGet(arr,left,right);
swap(arr,left,mid);
// 使用Hoare分区方法进行分区,并返回基准元素的位置
int par=partitionHoare(arr,left,right);
// 对分区点左边的子数组进行递归排序
quickSort(arr,left,par-1);
// 对分区点右边的子数组进行递归排序
quickSort(arr,par+1,right);
}
public static void InsertSort(int[] arr,int left,int right){
// 默认第一个元素是排好的
for (int i = left; i <=right; i++) {
// 将当前元素暂存,以便比较和移动
int temp=arr[i];
int j=i-1;
// 比较当前元素和已排序部分的元素,将大于当前元素的元素后移
for(;j>=left;j--){
if(arr[j]>temp){
arr[j+1]=arr[j];
}else{
break; // 当找到合适的位置时,跳出循环
}
}
// 将当前元素插入到正确的位置
arr[j+1]=temp;
}
}
5.5.3非递归实现快速排序
非递归快速排序,我们可以用栈来实现,栈的作用就是用来存放每次要进行排序时的左右边界值。
思路:
- 先给栈中压入左右两边界的下标值(这样才能保证栈内不为空,方便后序操作),这里先压左边的下标,再压右边的下标
- 弹出栈内的值,先弹出的为右边界,后弹出的为左边界值,调用排序,我们直接调用上述方法中的其中一个(Hoare、挖坑法、双指针法)
- 排完序后,我们需要进行分区(用par接收排序方法返回的分隔值)
- 判断par-1是否比左边界值(left)要大。(如果左边区间的元素少于两个,就不进行排序)。同理,判断以par+1为左边界,以right为右边界的区间内的元素是否有两个以上。
- 当栈内为空时,证明此时序列内已经排好序了。
class quicks {
public static void main(String[] args) {
int[] arr={1,2,3,4,5,6,7,8,9};
quick(arr);
System.out.println(Arrays.toString(arr));
}
public static void quick(int[] arr){
quickStack(arr,0,arr.length-1);
// quickSort(arr,0,arr.length-1);
}
public static void swap(int[] arr,int left,int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
/**
* 使用Hoare分区法进行数组分区。
*
* @param arr 待分区的整型数组。
* @param left 分区的起始索引。
* @param right 分区的结束索引。
* @return 返回分区点的索引。
* 这个方法采用了Hoare分区法来对数组进行分区操作,主要用于快速排序算法中。
* 它的选择基准值的方法是随机的,这有助于提高分区操作的效率,
* 通过交换数组元素,将小于等于基准值的元素放在基准值的左侧,大于基准值的元素放在右侧。
*/
public static int partitionHoare(int[] arr,int left,int right){
// 初始化基准值和左指针
int leftindex=left; // 用于存放基准值最终位置的指针
if(left>=right){
return left;
}
// 随机选择一个基准值位置,以提高分区效率
int rands=(int)Math.random();
rands%=(right-left+1);
rands+=left;
swap(arr,left,rands); // 将基准值与随机位置元素交换
int key=arr[left]; // 以左端点元素作为基准值
// 使用双指针法进行分区
while(left<right){
// 右指针向左寻找小于等于基准值的元素
while (left<right&&arr[right]>=key){
right--;
}
// 左指针向右寻找大于基准值的元素
while (left<right&&arr[left]<=key){
left++;
}
// 交换找到的左右元素,使小于基准值的元素集中于左侧,大于基准值的集中于右侧
swap(arr,left,right);
}
// 将基准值放置到正确的位置上
swap(arr,leftindex,left);
return left; // 返回基准值最终位置作为分区点
}
/**
* 使用快速排序算法的栈版本对数组进行排序。该方法采用了Hoare分区法。
*
* @param arr 要排序的整型数组
* @param left 分区起始索引
* @param right 分区结束索引
*/
public static void quickStack(int[] arr, int left, int right) {
// 使用栈来辅助进行排序,初始化栈
Stack<Integer> stack = new Stack<>();
stack.push(left);
stack.push(right);
// 栈不为空时持续进行分区和调整
while (!stack.isEmpty()) {
right = stack.pop(); // 取出右边界
left = stack.pop(); // 取出左边界
// 使用Hoare分区法进行分区
int par = partitionHoare(arr, left, right);
// 如果分区点左边有元素,则将左边界的下一个位置和右边界入栈
if (par - 1 > left) {
stack.push(left);
stack.push(par - 1);
}
// 如果分区点右边有元素,则将左边界和右边界的下一个位置入栈
if (par + 1 < right) {
stack.push(par + 1);
stack.push(right);
}
}
}
}
六.归并排序
6.1基本思想
归并排序是建立在归并操作上的一种有效的排序算法。利用分治思想,先使序列分成一个个子序列,将每个子序列进行排序,再将已经有序的子序列合并,得到一个完全有序的序列。若将两个有序表合并成一个有序表,称为二路归并。
6.2基本步骤
- 分解:将长度为n的序列分成两个长度为n/2的子序列,假设左边界为left,右边界为right。(利用递归)将序列不断进行分解,直到left>=right,说明此时序列已经分解完成。
- 排序合并:由于这里利用的是递归,当我们在分解完成后,要进行排序合并。假设mid为每次分解区间的中间值。我们需要将[left,mid]和[mid+1,right]这两个区间的元素进行排序,向内存申请right-left+1大小的空间(temp)。判断这两个区间的元素,依照升序依次存放到temp中,最后将排好序的temp复制到原数组中。
注意:在分解的过程中,排序合并操作是在递归回溯的时候是同时进行的!!!
动图演示:
6.3算法分析
时间复杂度:归并排序的最好、最坏和平均时间复杂度都为O(n*logn)。n为数组长度,在归并排序中总是将数组分为两半进行处理,每一次递归有n次比较和合并操作,共有n*logn层递归。
空间复杂度:O(n)。归并排序需要有个与数组大小相同的临时数组来帮助合并过程。
稳定性:稳定。合并过程中,会先把左边数组元素放到结果数组中,保持原有的相对顺序。
适用场景:大量数据的排序、外部排序、需要稳定排序的情况下,可以使用,但是其空间消耗较多。
6.4归并排序代码实现
6.4.1递归
import java.util.Arrays;
/**
* Merges类提供了用于对数组进行归并排序的方法。
*/
public class Merges {
public static void main(String[] args) {
int[] arr={1,5,8,65,4,78,12};
merger(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 对指定数组进行归并排序的入口方法。
* @param arr 需要排序的整型数组。
*/
public static void merger(int[] arr){
mergerFun(arr,0,arr.length-1);
}
/**
* 递归地将数组拆分并合并,实现归并排序。
* @param arr 需要排序的整型数组。
* @param left 分区的左边界。
* @param right 分区的右边界。
*/
public static void mergerFun(int[] arr,int left,int right){
// 如果左边界大于等于右边界,则直接返回,表示已经是最小单位或已完成排序
if(left>=right){
return;
}
int mid=(left+right)/2;
// 递归调用,对左右两个子区间进行排序
mergerFun(arr,left,mid);
mergerFun(arr,mid+1,right);
// 对两个已排序的子区间进行合并
mergerSort(arr,left,mid,right);
}
/**
* 将两个已排序的子数组合并为一个有序数组。
* @param arr 需要合并的整型数组。
* @param left 合并区间的左边界。
* @param mid 合并区间的中点。
* @param right 合并区间的右边界。
*/
public static void mergerSort(int[] arr,int left,int mid,int right){
// 创建临时数组用于存储合并后的结果
int[] temp=new int[right-left+1];
// 定义两个指针分别指向要合并的两个子数组的起始位置
int s1=left, e1=mid; // 第一个子数组的起始和结束位置
int s2=mid+1, e2=right; // 第二个子数组的起始和结束位置
int k=0; // 用于指向临时数组的当前位置
// 依次比较两个子数组的元素并将较小的元素放入临时数组,直到其中一个子数组被完全遍历
while (s1<=e1&&s2<=e2){
if(arr[s1]<arr[s2]){
temp[k++]=arr[s1++];
}else{
temp[k++]=arr[s2++];
}
}
// 将剩余未遍历的元素直接放入临时数组
while(s1<=e1){
temp[k++]=arr[s1++];
}
while(s2<=e2){
temp[k++]=arr[s2++];
}
// 将临时数组中的元素复制回原数组的相应位置
for(int i=0;i<k;i++){
arr[i+left]=temp[i];
}
}
}
这里在排完序后,临时数组复制给结果数组的时候,i不可以直接等于left,假设此时L=4,R=6,那么创建的临时数组可容纳的元素个数也只有R-L+1=3个,此时如果i为4的话,会造成数组越界。
6.4.2非递归实现
非递归和递归主要就是在分解的步骤不同。
基本思路:
- 定义一个变量gap=1,为每次归并间隔的大小。
- 进行分解,循环条件为:gap<n (n为数组长度)。
- 遍历数组,每次取两个相邻的子序列进行归并排序。
- 判断mid和right的下标是否超出了序列的长度,如果超出,将其赋值为序列的末尾元素的下标。
- 合并过程调用的方法与递归中2步骤相同。
- 将gap✖2,即gap=gap*2;
- 进行下一次归并排序,直到gap>n.
代码实现:
public class Merges {
public static void main(String[] args) {
int[] arr={1,5,8,65,4,78,12};
mergeNormal(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 将两个已排序的子数组合并为一个有序数组。
* @param arr 需要合并的整型数组。
* @param left 合并区间的左边界。
* @param mid 合并区间的中点。
* @param right 合并区间的右边界。
*/
public static void mergerSort(int[] arr,int left,int mid,int right){
// 创建临时数组用于存储合并后的结果
int[] temp=new int[right-left+1];
// 定义两个指针分别指向要合并的两个子数组的起始位置
int s1=left, e1=mid; // 第一个子数组的起始和结束位置
int s2=mid+1, e2=right; // 第二个子数组的起始和结束位置
int k=0; // 用于指向临时数组的当前位置
// 依次比较两个子数组的元素并将较小的元素放入临时数组,直到其中一个子数组被完全遍历
while (s1<=e1&&s2<=e2){
if(arr[s1]<arr[s2]){
temp[k++]=arr[s1++];
}else{
temp[k++]=arr[s2++];
}
}
// 将剩余未遍历的元素直接放入临时数组
while(s1<=e1){
temp[k++]=arr[s1++];
}
while(s2<=e2){
temp[k++]=arr[s2++];
}
// 将临时数组中的元素复制回原数组的相应位置
for(int i=0;i<k;i++){
arr[i+left]=temp[i];
}
}
/**
* 使用归并排序对数组进行排序。
* 该方法采用递归的方式,逐步缩小排序区间,最终完成整个数组的排序。
* @param arr 需要进行排序的整型数组。
*/
public static void mergeNormal(int[] arr){
int gap=1;
// 通过逐渐增加的gap大小,对数组进行多轮排序,每轮排序将数组划分为更小的区间
while(gap<arr.length){
// 遍历数组,以gap*2为步长,对每个区间进行归并排序
for(int i=0;i<arr.length;i+=gap*2){
int left=i;
int mid=left+gap-1;
// 处理区间中点越界的情况
if(mid>=arr.length){
mid=arr.length-1;
}
int right=mid+gap;
// 处理区间右边界越界的情况
if(right>=arr.length){
right=arr.length-1;
}
// 调用归并排序方法,对当前区间进行排序
mergerSort(arr,left,mid,right);
}
gap=2*gap; // 每轮排序后,gap大小翻倍
}
}
}
七.堆排序
前一章,我们已经讲解了有关堆的创建。堆排序是指利用堆积树这种数据结构所设计的一种排序算法,是选择排序的一种。
注意:如果要升序,需要先实现一个大根堆;排降序,先实现一个小根堆。
创建大根堆和小根堆可以看上一章堆
7.1基本思路
- 升序:先实现一个大根堆;降序:实现一个小根堆
- 让堆顶元素和堆尾元素交换,让有效个数-1
- 进行向下调整
重复1~3,直到排好序。
7.2算法分析
时间复杂度:O(n*logn). 建堆过程的时间复杂度为O(n),调整堆和交换操作需要进行n-1次,每次调整堆的时间复杂度为O(logn),
空间复杂度:O(1)。 原地排序,不需要额外的存储空间.
稳定性:不稳定。堆顶元素(最大或最小元素)与数组末尾元素交换时,可能会打乱相等元素的原始顺序。
适用场景:
- 大规模的数据:因为其时间复杂度为O(n*logn),所以在处理大量数据的时候,也能保持相对较高的性能。
- 内存限制:由于堆排序是原地排序算法,不需要额外的存储空间,所以在内存有限的情况下,堆排序是一个很好的选择。
- 对内存效率要求高:如果内存效率是首要考虑因素,且需要线性时间复杂度级别的排序算法,但对稳定性没有特殊要求,堆排序是一个不错的选择。
- 在线性时间复杂度内建立堆:堆排序的creatHeap方法可以在O(n)时间内建立堆,这对于需要快速初始化排序的数据流有帮助。
- 需要部分排序结果:在某些应用中,可能只需要部分数据排序,例如前k个最大或最小元素。堆排序可以轻松地在找到k个元素后停止,而不必对所有元素进行排序。
- 没有特殊性能要求:在没有特定性能需求(如稳定性、原地排序等)的场合,堆排序可以作为一个通用的排序算法来使用。
7.3代码实现
7.3.1升序
public class priorityss {
public static void heapSortB(int[] arr){
creatHeap(arr);
int useSize = arr.length-1;
while(useSize>=0){
swap(arr,0,useSize);
useSize--;
sitdownBig(arr,0,useSize);
}
}
public static void creatHeap(int[] arr){
for(int parent=(arr.length-1-1)/2;parent>=0;parent--){
sitdownBig(arr,parent,arr.length);
}
}
public static void sitdownBig(int[] arr,int parent,int end){
int child = parent*2+1;
while(child<end){
//找到左右子节点中较大的
if(child+1<end&&arr[child]<arr[child+1]){
child++;
}
if(arr[parent]<arr[child]){
swap(arr,parent,child);
//让p=c
parent=child;
child=parent*2+1;
}else{
//退出循环
break;
}
}
}
public static void swap(int[] arr,int parent,int child){
int temp = arr[parent];
arr[parent] = arr[child];
arr[child] = temp;
}
public static void main(String[] args) {
int[] arr={1,3,2,6,5,7,8,9,10};
heapSortB(arr);
System.out.println(Arrays.toString(arr));
}
}
7.3.2降序
public class priorityss {
public static void heapSortS(int[] arr){
creatHeap(arr);
int useSize = arr.length-1;
while(useSize>=0){
swap(arr,0,useSize);
useSize--;
sitdownSmall(arr,0,useSize);
}
}
public static void creatHeap(int[] arr){
for(int parent=(arr.length-1-1)/2;parent>=0;parent--){
sitdownSmall(arr,parent,arr.length);
}
}
public static void sitdownSmall(int[] arr,int parent,int end) {
int child = parent * 2 + 1;
while (child < end) {
//找到左右子节点中较大的
if (child + 1 < end && arr[child] > arr[child + 1]) {
child++;
}
if (arr[parent] > arr[child]) {
swap(arr, parent, child);
//让p=c
parent = child;
child = parent * 2 + 1;
} else {
//退出循环
break;
}
}
}
public static void swap(int[] arr,int parent,int child){
int temp = arr[parent];
arr[parent] = arr[child];
arr[child] = temp;
}
public static void main(String[] args) {
int[] arr={1,3,2,6,5,7,8,9,10};
heapSortS(arr);
System.out.println(Arrays.toString(arr));
}
}
八.计数排序
8.1基本思想
计数排序的核心在于将输入的数据转化为键值存储在额外开辟的空间中,计数排序要求输入的数据必须是有确定的范围。
8.2基本步骤
- 定义两个变量 ,min,max,以便创建在此范围内用来存放键值的数组。
- 遍历待排序的数组,将最小值放进min,将最大值放进max。
- 创建一个【max-min+1】的键值数组。
- 遍历待排序数组,将数组中的元素-min作为键值数组的下标,统计每个元素的出现次数。
- 将键值数组中不为0的元素,按照其有个数,输出其下标个数。(在输出的时候,要加上min,因为在存储的时候,我们把min减去了)
动图演示:
8.3代码实现
package priorityQueue;
public class counts {
public static void countSort(int[] arr){
int max=arr[0];
int min=arr[0];
for(int i=0;i<arr.length;i++){
if(min>arr[i]){
min=arr[i];
}
if(max<arr[i]){
max=arr[i];
}
}
int[] count=new int[max-min+1];
for(int i=0;i<arr.length;i++){
count[arr[i]-min]++;
}
int k=0;
for(int i=0;i<count.length;i++){
while(count[i]>0){
arr[k]=i+min;
System.out.print(arr[k++]+" ");
count[i]--;
}
}
}
public static void main(String[] args) {
int[] arr={54,65,4,65,4,78,87,8,7,8,79,8,654,2};
countSort(arr);
}
}
排序优化
看图中,我们将键值中的C[i]转换为小于i的元素个数 ,例如,在键值数组C中,小于等于1的个数有4个,那么以下标为1的个数就是前一个元素➕当前元素的个数。
再创建一个B数组(长度与A数组相同),那么我们在A数组中从后往前遍历,将A数组的元素作为C数组的下标,得到C数组中的元素后,将其作为B数组的下标(这里因为下标是从0开始的,所以要减1),把此时A数组的元素赋值给此时的C数组。
代码实现:
/**
* 使用计数排序算法对整数数组进行排序。
* @param arr 待排序的整数数组。
* 注意:该方法会修改原数组内容,并通过标准输出打印排序结果,不返回排序后的数组。
*/
public static void countSorta(int[] arr){
// 查找数组中的最大值和最小值
int max=arr[0];
int min=arr[0];
for(int i=0;i<arr.length;i++){
if(min>arr[i]){
min=arr[i];
}
if(max<arr[i]){
max=arr[i];
}
}
// 创建计数数组,长度为最大值与最小值之差加1
int[] count=new int[max-min+1];
// 对每个元素,将其在计数数组中对应的位置计数加1
for(int i=0;i<arr.length;i++){
count[arr[i]-min]++;
}
// 计算累加和,以便确定每个元素在输出数组中的位置
for(int i=1;i<count.length;i++){
count[i]+=count[i-1];
}
// 根据计数数组构建排序后的数组
int[] C=new int[arr.length];
for(int i=arr.length-1;i>=0;i--){
C[count[arr[i]]-1]=arr[i];
count[arr[i]]--;
}
// 打印排序结果
for(int i=0;i<C.length;i++){
System.out.print(C[i]+" ");
}
}
其他排序
总结
若有不足欢迎指正~