日升时奋斗,日落时自省
目录
1、插入排序
2、希尔排序
3、选择排序
4、堆排序
5、冒泡排序
6、快速排序
6.1、Hoare法找基准值
6.2、挖坑法找基准值
6.3、快慢指针找中间值
6.4、优化
6.5、非递归排序
7、归并排序
7.1、递归实现
7.2、非递归实现
7.3、海量数据排序问题
8、计数排序
9、桶排序
10、基数排序
排序有多种方法,排序被分为比较排序和非比较排序
所谓排序就是使一串记录 ,按照其中某个或者某些关键字的大小进行排序,递增或者递减的排列的操作
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变
内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能一次性放在内存进行过排序,根据排序过程的要求不能在内外存之间进行移动数据的排序
扑克牌在整理牌的过程中,都先把牌先排好,方便自己出牌,一般都会从小到大进行排序,每次揭牌时,都会进行比对,与当前手牌进行比对,如果比当前手牌大那就放在当前位置,如果比的那个钱手牌小就再次向后进行比对
思路:
(1)你接到第一章牌是不用排序,此时会让我们手中有一张牌
(2)你接到第二章牌就会和前一章牌对比较大小
(3)小于前一章牌,当前手牌向后一个位置
(4)大于前一章手牌,那就停在当前接牌的位置
(5)重复此操作
插入排序的代码解析:
private static void InsertSort(int[] array) {
for (int i = 1; i < array.length; i++) { //这里摸取的是下一个手牌
int tmp=array[i]; //下一个手牌也是临时的手牌 因为还没有确定的位置
int j=i-1; //当前手牌的第一张牌 其实也是手牌里最大的牌
for(;j>=0;j--){ //注意: 刚刚说了j=i-1 是当前最大手牌的下标 ,所以这里的j-- 如果摸牌小于的当前手牌就会向更小手牌的位置·1进行比对
if(array[j]>tmp){
array[j+1]=array[j];
}else{ //如果 发现当前手牌有比接来的手牌更小 那就结束
break;
}
}
array[j+1]=tmp; //结束循环后 需要将摸到牌 放到 当前手牌的前面
}
}
注:这里针对几个细节点友友们需要注意一下:
(1)第二个for循环的j>=0 ,因为要与当前手牌的最后一张牌进行比对,所以0下标也是一张手牌,也需要比对
(2)最后一行 array[j+1]=tmp 此处赋值为什么是给j+1下标 ,因为for循环中找到是当前手牌小于摸取时结束循环,也就是接取牌大于当前手牌应该放在当前手牌的前面==当前手牌下标+1
时间复杂度:O(n^2) (以下是时间复杂度的解析)
空间复杂度:O(1) : 没有利用其他空间
稳定性:稳定
希尔排序又称为缩小增量法,希尔排序的基本思想:先选定一个整数 ,把待排序文件中所有记录分个组,以距离为记录分在同一组内,并对每一组内的记录进行排序,然后,缩小距离再次进行缩小距离,重复以上操作,直到当分组等于1时,相当于一次插入排序,该次结束后就排序好了
下面以距离为5进行分组,分为5组演示
然后将距离缩进再次进行排序,此次是以距离为2分组,分为两组
以上分组可能有些纠结,不知道怎么分,因为涉及到一个尚未解决的数学问题,所以没有具体的分组,但还是有大佬给出了一些分组方法(这些方法并没有被认证),有人主张gap(分组)=n/2(这里n表示元素总个数)或者gap=(gap/3)+1 ;基本我们也上采取了以上方法,当然友友们还可以去科普一下还有其他的分组方法(此处代码是以除2来分组的)
private static void shell(int[] array, int gap) {
for(int i=gap;i=0;j-=gap){ //与插入排序的不同之处就是 以gap递减
if(array[j]>tmp){
array[j+gap]=array[j]; //所以这里的移动也是 以gap(分组)进行的
}else{
break;
}
}
array[j+gap]=tmp;
}
}
private static void ShellSort(int[] array) {
int gap=array.length; //有所元素的个数
while(gap>1){ //当 分组等于 1 的时候 循环结束 ,当然了分组等于1 也同样会进行的
gap/=2; //这里采取的分组是 以除2 划分 每次除以2 直到为gap=1
shell(array,gap); //分组后就进行一次排序
}
}
注:解释细节:每次分组后进行排序,排序类似于插入排序,每次都是一个分组一个分组进行比较,第一个for循环中i=gap是起始位置,那为什么进行的是i++,不是i+=gap,因为每个分组都需要排序,所以i++可以走完所有元素,但是以gap只能走完一组元素。
提示:希尔排序的时间复杂度没有具体的,但是有大佬给推测了也都没有被证实,希尔排序的时间复杂度暂时被定义在O(n^1.25~n^1.6) 这里我们可以认为希尔排序时间复杂度大约为O(n^1.3)
空间复杂度: O(1)
稳定性:不稳定
基本思想:每次从待排序的数据元素中选出最小(或者最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排序完成(以下一个简单的序列演示)
private static void swap(int[] array, int j, int i) {
int tmp=array[j];
array[j]=array[i];
array[i]=tmp;
}
private static void SelectSort(int[] array) {
for (int i = 0; i < array.length; i++) { //有多少个元素就 需要多少次排序
int mindex=i; //每次排序 都会到最小值小标,当前先以第一个位置为假设的最小值
for (int j = i+1; j
以上的代码是固定的写法吗???当然不是,下面在介绍一种方法同样是选择排序,使用两个指针前后跑
思路:
(1)两个指针分别从数组两边开始,每次排序最小值都往当前序列的最左侧放置,每次排序最大值都往当前序列的最右侧放置
(2)当前序列最小值的下标记录来最终与最左边的值交换
(3)当前序列最大值的下标记录来最终与最右边的值交换
以上图解演示以只有部分过程,后面所有部分都会重复当前过程
private static void swap(int[] array, int j, int i) {
int tmp=array[j];
array[j]=array[i];
array[i]=tmp;
}
public static void selectSortnew(int[] array){ //新思路选择排序
int left=0; //最开始 的左边下标
int right= array.length-1; //最开始的右边下标是数组长度-1
while(leftarray[maxdex]){ //找到最大下标记下来
maxdex=j;
}
}
swap(array,mindex,left); //这里最小的先交换
if(maxdex==left){ //需要把最大值换出去
maxdex=mindex; //那时的最小值小标就是最大值直
}
swap(array,maxdex,right); //最大的再交换
left++; //已经确定本次循环的最小值和最大值在最左边和最右边跳过这两个位置
right--;
}
}
时间复杂度 : O(n^2)
空间复杂度:O(1)
稳定性:不稳定
堆排序是利用堆积树这种数据结构所设计的一种排序算法,堆排序是选择排序的一种,这里排序需要注意的排升序要建大根堆,排降序建小根堆
基本思路:
(1)建立一个升(降)序的排序,就需要先建立一个大(小)根堆,为升序做一个铺垫
(2)大(小)根堆:堆顶为最大(小)元素,与该堆的最后一个元素换位置就可以让最后一个元素变成最大(小)的,此时有效值个数减减
(3)调整大(小)根堆,堆顶得到第二大(小)元素,再次重复以上操作
代码解析(带有详细的注释):
private static void heapSort(int[] array) {
createHeap(array); //首先要创建一个堆
int end=array.length-1; //当大根堆建成之后,每次都会将最后一个位置置换掉
while(end>0){ //结尾位置如果到了 下标为0 的话 就进行结束 因为0位置不用置换
swap(array,0,end); //堆顶可以和有效末进行交换
shiftDown(array,0,end); //但是交换之后 ,堆就需要重新堆排序
end--; // 交换后 就需要将有效值-1 因为当前位置已经算是排序好了
}
}
private static void createHeap(int[] array) {
for(int parent=(array.length-1-1)/2;parent>=0;parent--){ //从最后一个父节点 开始前进行,直到
shiftDown(array,parent,array.length); //数组进行向下调整
}
}
private static void shiftDown(int[] array, int parent, int len) {
int child= 2*parent+1; //父节点 知道了 需要知道子节点 如果子节点大于父节点 就会进行置换
while(child
时间复杂度: O(nlogn) 以下是堆排序的时间复杂度解析
空间复杂度 : O(1)
稳定性: 不稳定
冒泡排序是基于交换排序而来,交换排序的基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置;交换排序特点:将键值较大的记录下来向序列的尾部移动,键值较小的记录向序列的前部移动(这里也是基本思路)
代码解析(附加有注释):
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1; i++) { // 这里是 n 次排序
boolean flag=false; //优化 判断 如果是一个有序的序列
for(int j=0;j
时间复杂度 :O(n^2) 优化措施不是 优化最坏的时间复杂度是而是最好的时间复杂度 ,最好时间复杂度 O(n)
空间复杂度 : O(1)
稳定性 : 稳定
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
类似于二叉树的特点:上面叙述就像是一个二叉树一样,左子树和右子树,先找到一个基准值把左右树分开,再向左右树找到基准值,又作为当前树的根节点,左右树会越来越有序
简单思路:
(1)首先创建一个快排 为了方便传参 当前数组、数组开始位置、数组结束位置
(2)此处使用了递归快排方法,递归结束就是开始位置 如果大于等于 结束位置 递归结束了
(3)写一个方法 找中间值,再向两边进行类似于二叉树,左右开工,递归以上操作
private static void quickSort(int[] array) {
quick(array,0,array.length-1); //进行快排
}
private static void quick(int[] array, int start, int end) {
if(start>=end){ //判定递归结束 标志
return;
}
int pivot=partitionPoint(array,start,end); //找中间值
quick(array,start,pivot-1); // 左侧序列进行递归排序
quick(array,pivot+1,end); // 右侧序列进行递归排序
}
以上就是基本的操作了 ,下来该写partitionPoint方法 找中间值(基准值),该处使用Hoare方法
思路:
(1)找左序列大于基准值,找右序列小于基准值
(2)左右序列找到对应值后,进行交换
(3)重复以上操作
private static int partitionHoare(int[] array, int left, int right) {
int i=left; //以左值为基准值 记录当前下标
int pivot=array[left]; //不仅记录当前位置 同时也要记录左值
while(left=pivot){ //右侧先走 判断右侧值小于基准值 找到后就停留在此处的下标
right--;
}
while(left
思路:
(1)挖坑法 就是 将当前基准值 挖出
(2)右序列需要小于基准值就填补到左侧挖空的位置,此时右侧该位置就空了
(3)左序列需要大于基准值就填补到右侧挖空的位置,此时左侧该位置就空了
(4)重复以上操作
private static int partitiondag(int[] array, int left, int right) {
int i=left; // 和Hoare方法一样 记录好 左侧值的小标
int pivot=array[left]; // 同时记录了 左侧值也会被记录下来
while(left=pivot){ //右侧找到 右侧序列 大于 基准值
right--;
}
array[left]=array[right]; //找到右侧值 就把 左侧值的空缺填补上
while(left
以上涉及的问题 : array[right]>=pivot 此处如果把等号去掉能行吗?实际上不行,如果左右值都相等,此处不加等号 那还能动吗???(以下进行解释)
快慢指针是这里不太常用的方法 ,在我们学习链表刷题就会看见;
思路:
(1)指针一(慢指针)是当前位置 ,指针二 (快指针)是当前位置的下一个位置
(2)指针二(快指针)大于基准值的话指针一就不会移动 ,只有指针二(快指针)向前走,如果指针二(快指针)小于基准值的话,两者还是相邻,一块走
(3)如果相邻的话,就执行指针二(快指针)走一步,指针一(慢指针)跟随指针二(快指针);如果不相邻的话,就执行指针二(快指针)走
(4)重复以上操作
private static int partitionPoint(int[] array, int left, int right) {
int cur=left+1; //指针二
int prev=left; //指针一
while(cur<=right){
//指针二是否小于基准值 并且 指针一是否与指针相邻
if(array[cur]
时间复杂度 : O(n^2)
如果快排进行子序列比较小的话,就会导致快排变慢 ,为什么变慢因为递归有一定的限制,次数多了很难带得动,会慢慢趋向类似于冒泡排序
(1)优化1 ,在一定范围内就可以开始调节成插入排序
(2)优化2 ,单序列也就是有序的序列导致了当前快排速度会降下来,所以这里采取三数取中法,将有序序列转换为类似于二叉树类型
再原有的代码上添加部分代码
private static void quick(int[] array, int start, int end) {
if(start>=end){ //判定递归结束 标志
return;
}
//插入排序 适合数据少,越排越有序此时的速率比快排本身更快
if(end-start+1<15){
selectSort(array,start,end);
}
//优化 三数取中法
int index=findMind(array,start,end);
swap(array,start,index);
int pivot=partitionPoint(array,start,end); //找中间值
quick(array,start,pivot-1); // 左侧序列进行递归排序
quick(array,pivot+1,end); // 右侧序列进行递归排序
}
private static void selectSort(int[] array, int start, int end) {
//插入排序 但在此处是需要注意位置,从当前排序序列开始,当前序列末尾位置结束
for (int i = start+1; i <= end; i++) {
int tmp=array[i];
int j=i-1;
for(;j>=0;j--){
if(array[j]>tmp){
array[j+1]=array[j];
}else{
break;
}
}
array[j+1]=tmp;
}
}
private static int findMind(int[] array, int start, int end) {
int mid=(start+end)/2;
if(array[start]>array[end]){
if(array[mid]>array[start]){
return start;
}else if(array[mid]>array[end]){
return mid;
}else{
return end;
}
}else{
if(array[start]>array[mid]){
return start;
}else if(array[end]>array[mid]){
return mid;
}else{
return mid;
}
}
}
三数取中法思路:
时间复杂度优化后: O(nlogn)当前在什么情况下都会尽量被化成类似二叉树时间复杂度也就是O(logn)
空间复杂度 : O(1)
稳定性: 不稳定
思路:
(1) 建立了一个栈,将序列分别成为做左序列和右序列
(2)栈存储的是左序列的开始下标和结束下标,还有右序列的开始下标和结束下标
(3)以上操作结束后就开始接收抛出栈的下标,第一个下标就是当前序列的结束位置,第二个下标就是当前序列的开始位置
(4)序列被分开后进行了方法找基准值,到此就已经结束了,重复操作即可
代码解析(附加注释)
private static void quickSort(int[] array) {
Stack stack=new Stack<>();
int start=0; //当前序列一个开始位置
int end=array.length-1; //当前序列一个结束位置
//找中间位置 方法还是原来的三种方法
int pivot=partitionHoare(array,start,end);//进行找基准值
if(pivot>start+1){ //此时需要记录左序列的开始 和 结束的下标
stack.push(start); //存放左序列的开始位置下标
stack.push(pivot-1); //存放左序列的结束位置下标
}
if(pivotstart+1){ //再次划分对序列进行划分 左右序列 并记录下左序列的初 末下标
stack.push(start);
stack.push(pivot-1); //数组 除了中间值 前后部分都需要进行操作
}
if(pivot
注:如果使用起来的话数据少的情况下可以选择递归因为快,如果数据多了,还是选择非递归更保守
归并排序:该排序采用分而治之的思想进行实现,先分解,进行有序化,在进行合并成有序序列,此处解释(l就是left r就是right)图解
思路:
(1)先将序列分开 ,如何分开此处代码选择的是递归进行分开 ,其实类似于快排的递归,但是有点点差距,就是快排是基准值左右分开,把基准值不分任何一边,但是归并是将左边和右边分开,以中间值为基准,左序列包含中间值,右序列不包含中间值
(2)递归完成后进行排序整合,写一个方法进行排序并整合 ,这里采用的是两数组进行过合并
代码解析(附加有详细注释):
private static void mergeSort(int[] array) {
//此处是为了方便有初始值,这么写的
mergeSortChild(array,0,array.length-1);
}
private static void mergeSortChild(int[] array, int left, int right) {
//以左小标大于等于 右下标为结束标记 为什么有大于号 因为如果当前数组是一个元素的话
//就会导致 left=mid+1=1 right=0 的情况 丢失
if(left>=right){
return ;
}
//找中间值 是下标为中间位置的值
int mid=(left+right)/2;
//递归的目的就是为了 序列进行类似二叉树的分解
mergeSortChild(array,left,mid);
mergeSortChild(array,mid+1,right);
//下面方法就是 合并,分段合并 ,进行重整
merge(array,left,mid ,right);
}
private static void merge(int[] array, int left, int mid, int right) {
//既然是合并 那就是两组的事情
//以下是第一个数组的 开始位置 和 结束位置
int start1=left;
int end1=mid;
//以下是第二个数组的 开始位置 和 结束位置
int start2=mid+1;
int end2=right;
//建立一个新数组 为了将排序好的序列存好 个数也就是当前两个序列的所有元素个数
int[] tmpArray=new int[right-left+1];
//记录新数组的 下标位置
int k=0;
//在两个数组分别都没有结束的情况下 进行对比,谁小谁下放入新数组中 自然排序
while(start1<=end1&&start2<=end2){
if(array[start1]>array[start2]){
tmpArray[k]=array[start2];
k++;
start2++;
}else{
tmpArray[k]=array[start1];
k++;
start1++;
}
}
//也有可能有情况 一个数组走完了 ,另一个数组没有走完的情况 衔接上就行了
while(start1<=end1){
tmpArray[k]=array[start1];
k++;
start1++;
}
//此处与上同理
while(start2<=end2){
tmpArray[k]=array[start2];
k++;
start2++;
}
//为什么会有以下操作 这里就是 整合 将新数组整合后给原来数组 下标位置
for(int i=0;i
思路:
(1)非递归也是遵循递归的路子来的,只是可以说是倒着理解
(2)从一组一组分开,然后合并成一个大组,以上遵循此操作
代码解析(代码附加详细注释):
private static void mergeSort(int[] array) {
//非递归就是 以一半一半来解决问题
//gap分组 将一个元素分为组
int gap=1;
//一组个数在不大于全部元素的个数就还能分 如果大于也就结束了
while(gap=array.length){
mid=array.length-1;
}
//这里right与mid的同理
if(right>=array.length){
right=array.length-1;
}
//解决完 递归带来的序列分组后 开始合并整理 此处不变
merge(array,left,mid,right);
}
//当以元素个数为1 分组后 合并成两个元素数组 所以再分组就是以gap*2
gap*=2;
}
}
private static void merge(int[] array, int left, int mid, int right) {
int start1=left;
int end1=mid;
int start2=mid+1;
int end2=right;
int[] tmpArray=new int[right-left+1];
int k=0;
while(start1<=end1&&start2<=end2){
if (array[start1]
注:合并整理的代码都是一样的,只要在看归并排序(递归方法)看懂就行,这里只需要理解如何将递归表示的意思用非递归的方法表示
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G 需要排序数据有100G
内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
(1)首先 : 文件分为200份,每个0.5G
(2)分别对0.5G排序,因为内存已经可以放的下,任意排序都是可以
(3)进行归并处理,同时对200份有序文件归并,解决问题
思想:计数排序又称为鸽巢原理,是对哈希定址法的变形应用
(1)统计相同元素出现的次数
(2)根据统计的结果将序列回收到原来的序列中
代码解析(详细注释):
private static void countSort(int[] array) { //计数排序 以空间换时间 适合集中数据使用
int maxVal=array[0];
int minVal=array[0];
for(int i=0;imaxVal){
maxVal=array[i]; //找最大值
}
}
for(int i=0;i< array.length;i++){
if(array[i]
时间复杂度 :O(n)
空间复杂度 :O(n)
稳定性:稳定
思路:划分多个范围相同的区间,每个子区间自排序,最后合并
(1)创建几个桶,也是看自己怎么定可以是10个,也可以是6个
(2)将数据进行处理,分别对应不同的桶放入
(3)将每个桶都进行排序
(4)依照顺序将每个桶都释放出来
private static void radixSort(int[] array) {
//创建6个桶 简单说是创建了6个顺序表,每个数组都是一个顺序表
ArrayList[] bucker=new ArrayList[6];
//简单说是创建了6个顺序表,每个数组都是一个顺序表
for(int i=0;i();
}
for (int i=0;i< array.length;i++){
//将数据进行处理
int index=array[i]/10;
//然后对应桶号也就是数组下标 放入桶中
bucker[index].add(array[i]);
}
int index=0; //新下标 重新赋值给原数组
for(int i=0;i< bucker.length;i++){
//从0开始 进行 for循环 将每个桶都进行排序
bucker[i].sort(null);
//然后将当前桶排序好的数值放到数组中
for(int j=0;j< bucker[i].size();j++){
array[index]=bucker[i].get(j);
index++;
}
}
}
时间复杂度 : O(nlog(n/m))
空间复杂度 : O(n+m) 空间复杂度适应
稳定性:看处理单桶的排序是否稳定取决于此排序使用什么样的排序算法
提示:知道即可,想深究的友友可以好好理解一下
基数排序是一种比较整型数据排序的算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较,从个位到最大值的最大位;可以看出基数比较是通过从头到尾进行比较的,其实挺像字符串进行比较大小的,从当前位置比较到最后,此处也是一样的,只不过是从个位开始到最大位,所以基数排序不是只能使用于整数排序。
思路:
(1)基数排序是根据位数来处理的,首先计算最大值有多少位,此处就要处理几次
(2)建立桶,跟桶排序一样,将各元素的个位数进行比较,分别放到10个桶中
(3)从0号桶中逐一取出按照顺序,再次进行十位数比较,分别放到10个桶中
(4)重复以上操作,最大值有多少位进行多少次
代码解析(详细注释):
public int[] sort(int[] sourceArray){
int[] arr= Arrays.copyOf(sourceArray,sourceArray.length);
//记录下最大值是几位数
int maxDigit=getMaxDigit(arr);
//基数排序开始进行
return radixSort(arr,maxDigit);
}
private int[] radixSort(int[] arr, int maxDigit) {
int mod=10; //此处可以有 就是为了防止有0-9对应的负数,
int dev=1; //此处表示个位数 累积乘10 为了取得每个位置的数
for(int i=0;i
时间复杂度 :O(n)
空间复杂度 :O(n)
稳定性: 稳定
11、排序总结
以下是7种排序算法时间复杂度和空间复杂度以及稳定性的
提示:针对希尔排序算法,时间复杂度可以大概记为O(n^1.3),因为希尔排序在数学上并没有具体证实,所以有很多的人对其有解释,这里只是提示友友们。
排序 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
冒泡 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
插入 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
希尔 | O(n) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(1) | 不稳定 |
快速 | O(n*log(n)) | O(n*log(n)) | O(n^2) | O(log(n)~O(n)) | 不稳定 |
归并 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(n) | 稳定 |
以上有细节错误,还希望大家评论提示