原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9082138.html

一、概述

  基础排序算法包括:桶排序、冒泡排序、选择排序、插入排序等

二、桶排序

2.1 算法介绍

  桶排序可以算是最简单快速的排序算法了,只是限定条件要多一点,需要事先知晓待排序列的极限值或范围来准备足够的桶。

2.2 算法原理

  桶排序的原理就是准备足够数量的有序桶(一般用数组实现),用于标记待排序列的每个元素,用元素值对应桶下标,桶里的值代表的是元素对应的值出现的次数,有一次就在原值上加1(初始值为0)。当将所有的待排序列中的元素遍历一遍后,将其全部标记到桶中,这时候其实就已经排好序了。如果我们需要正序排序,则对桶进行正序遍历,排除值为0 的桶,按顺序和桶中值的个数输出桶的下标值,即为正序列,倒序反之。

2.3 算法实现

 public class BusketSort {

   public static void sort(int[] ints, Boolean isAsc) {
int[] basket = new int[101]; //定义足够大的数组桶
for (int i : ints) {
basket[i]++; //对应元素的桶下标自增
}
if (isAsc) {
for (int i = 0; i < basket.length - 1; i++) {
if (basket[i] > 0) {
for (int j = 1; j <= basket[i]; j++) {
System.out.print(i + " "); //循环输出桶元素不为0的下标值
}
}
}
} else {
for (int i = basket.length - 1; i > 0; i--) {
if (basket[i] > 0) {
for (int j = 1; j <= basket[i]; j++) {
System.out.print(i + " "); //循环输出桶元素不为0的下标值
}
}
}
} } public static void main(String[] args) {
int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};
sort(ints, false);
}
}

2.4 算法解析

  首先我们需要准备一个桶数组,桶数组的大小为已知的待排序列的范围,比如我们要对学生的单科成绩(满分100)进行排序,那么我们就要准备101个桶(即定义一个长度为101的数组)。

  然后遍历待排序列,将与待排元素值一致的桶下标对应的桶的值加1(初始值为0),遍历结束,其实排序也就完成了。

  最后,我们需要正序排序则,正序遍历桶输出桶下标,倒序则倒序遍历桶输出桶下标(需要注意的就是下标输出的次数为下标对应的桶值)。

  桶排序的时间复杂度是O(m+n),其中m为待排序元素个数,n为桶数。

  桶排序的空间复杂度极大,与待排序的最大元素相关。

  从代码实现中我们也可以看出,桶排序是及其简单的,唯一的缺陷就是桶的存在及其占用空间,如果我们要排序的数列只有很少的个数,但是元素之间相差极大的话,那么我们就需要准备一个极大的桶,及其浪费空间资源。一般情况下我们会在一个桶中存放一个范围之内的元素,桶中的元素我们可以采用其他的排序算法实现排序。这样可以极大的降低空间消耗。

  桶排序一般适用于如下场景:

     数据分布相对较为均匀或者数据范围不大的情况下

    特殊场景,比如需要统计元素的个数的情况,或者希望通过哈希映射快速获取某些值的情况

三、冒泡排序

3.1 算法介绍

  冒牌排序是很耳熟的排序方式,虽然它使用的很少,但是经常会出现在面试中,冒泡的意思是渐进式的意思,即渐进式排序。

3.2 算法原理

  冒泡排序就是通过相邻元素的两两比较交换的方式实现排序,每次比较的两个元素中都包含上一次比较的后一位元素,也就是每次比较替换之间都是存在关联的,那个关联的元素一步步向后移,就好比冒泡一般,直到水面(排好序)。

  每次冒泡的结束都意味着一个元素的归位,即一个元素被排好序,一般我们都是采用向后冒泡的方式来实现排序的,那么也就意味着,元素归位的顺序是从尾部开始的。

  而每次冒泡又都是从首位元素开始的,到未被排序的最后一位结束。那么也就意味着每次冒泡的比较次数要比上一次冒泡少1次,因为每结束一次冒泡就会在末尾完成一个元素的排序,而未排序的元素相应的就会少一位。

  了解上面的这些,这有利于我们实现编程算法,但初学者必定是一头雾水,不要急,你需要在参考下面实现源码的基础上再理解上面的内容,那么你会加深理解。

3.3 算法实现

 public class BubbleSort {

     public static void main(String[] args) {
int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};
sort(ints);
} public static void sort(int[] ints){
for(int i = ints.length - 1; i > 0; i--) {
for(int j = 0; j < i; j++) {
if(ints[j] > ints[j + 1]){
int temp = ints[j];
ints[j] = ints[j + 1];
ints[j + 1] = temp;
}
}
}
for(int i : ints) {
System.out.print(i + " ");
}
}
}

3.4 算法解析

  从上面的代码中我们进行原理的解析,这样可以加深对原理的理解,我们学习算法并不是为了背诵这么一段简单的代码,我们学的是原理,是思维,虽然我也无法完全做到,但努力中,因为在我们完全理解了其原理之后,代码实现起来就会相当相当的容易,再也不用去背代码。

  冒泡排序的实现代码中第9行为外层循环,用于控制冒泡的次数,正如原理中所说,冒泡排序是从尾部开始的,所以我们的循环变量i从尾部开始循环,末尾元素下标即为ints.length-1,i的变化范围为i>0,这里不包含0 的原因是因为当我们把序列的ints.length-1个元素排好序之后,只剩余一位元素,那么这一位元素自动就已经排好序了,无需再次冒泡,若为从小到大排序,那么最后剩余的元素就是首位元素,它即为最小的元素,不用再次执行冒泡排序。那么我们就得知外层循环控制的冒泡的次数为序列的长度减1次。

  第10行的内容为内层循环,由于每次冒泡我们都是从首位元素开始,慢慢右移到末位(这里的末位并非末尾),一次冒泡是由有数次的两两比较交换(第11到14行代码)实现的,所以我们的代码中内循环变量j=0,表示每次都是从0下标元素开始冒泡,j的变化范围为j <i,这里是我们的重点,是内外层循环的关联点,我们每次冒泡比较的次数都是不同的,细心点就会发现,每次次数都会少一次,这个逻辑的控制就在这里,j小于i,而i在每次冒泡开始前都会执行i--,正好实现了我们的目的,而正因如此,我们每次冒泡结束的时候并不是末尾,而是未排序元素末位,这个末位会随着冒泡逐次前移,每次一位,直到第二位进行末次冒泡进而完成排序。

  忘了说一点,这里j<i,为什么不是i-1,或者i+1呢?因为在我们每次冒泡的最后一次比较中,都是末位和末位前的两位元素比较,然后就结束了,并不是比较剩余未排序元素个数次,而是个数减1次,这里j<i,正好就是减1次。

  冒泡排序的精髓就在这两层循环之内,完全理解其意之后,实现起来就很简单了。

  冒泡排序外层循环执行次数为n-1次,这里n代表序列元素个数,内循环次数为n(n-1)/2次,冒泡排序算法的时间复杂度为O(n2)。

  冒泡排序算法中会用到临时空间用于元素交换,通过优化,我们可以实现,空间复杂度为O(1)。

  冒泡排序现在更多的是出现在一些面试题里面,用于查看面试者的只是扩展度,实际使用中很少用到了,因为这种算法的时间复杂度还是不如人意。现在使用较多的还是快速排序算法。

四、选择排序

4.1 算法介绍

  选择排序,就是选择,我们在一堆元素中进行有目的的挑选,然后用挑选到的元素实现排序。

4.2 算法原理

  接上述,如果我们要实现序列正序排序,那么我们可以在元素堆中选择最小的元素将其放到首位,然后在剩余的元素中再次挑选最小的元素,将其放到第二位,以此类推,直到倒数第二个元素被挑选出来,整个序列也就是先了正序排序。因为每次都是选择最小的元素,那么剩余的最后一个元素一定是最大的元素,所以最后一个元素无需再进行挑选替换。

  这里说到替换,其实每次将选到的元素放到正确的位置都是与其位元素进行替换实现。

  每选择替换一次则意味着排好一位元素,由此可知,元素是从前往后实现排序的。

4.3 算法实现

 public class SelectSort {

     public static void main(String[] args) {
int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};
sort(ints);
} public static void sort(int[] ints){
for(int i = 0; i < ints.length - 1; i++) {
for(int j = i + 1; j < ints.length; j++){
if(ints[i] > ints[j]) {
int temp = ints[j];
ints[j] = ints[i];
ints[i] = temp;
}
}
}
for(int i : ints) {
System.out.print(i + " ");
}
} }

4.4 算法解析

  选择排序就是每次在剩余元素堆中选择最小的元素,但在实现方式上是通过比较替换的方式实现的,每次再剩余的元素中选择首位元素,这个元素的位置其实就是当次循环的最小元素归位位置,用这个元素逐个与其后的元素进行比较替换,直到将该位变成当前最小元素。

  外层循环每完成一次,就可以归位一个元素,内层循环则是用于通过比较替换来实现归位,外层循环从首位开始进行排序,所以i=0,其循环的次数为元素个数-1次,是因为只剩最后一个元素的时候其实就已经完成了所有元素的排序工作了,最后一个元素无需再次循环。内层循环的开始是i+1,表示每次内循环都是在外循环开始的循环变量指代的位置的下一位开始与外循环开始的循环变量指代的位置进行比较替换,这种比较操作需要持续到最后一位元素。所以j<ints.length。

  时间复杂度还是O(n2)。

  空间复杂度可达O(1)。

五、插入排序

5.1 算法介绍

  插入排序,也是一种简单的排序方法,通过顺序选择元素与已排好序的元素进行比较替换实现全部排序。

5.2 算法原理

  插入排序从第二个元素开始进行,因为第一个元素默认排好序,因为它只有一个元素,自动有序,从第二个元素开始与之前的元素逐个比较替换进行排序。这个比较替换遵循以下规则:

    如果该元素比前一个元素(已有序的最后一个元素,即当前最大元素)大,则自动排在末尾,不再与之前的元素进行比较替换,充当当前的最大元素;

    如果该元素比前一个元素小,则将该元素提取出来,开始与之前的元素进行逐个比较替换,直到找到比当前元素小的元素或者到达第一位为止;

    如果找到了比当前元素小的元素,说明当前元素是整个序列中非首和尾元素(即不是最小元素也不是最大元素),则将找到的元素之后已排序元素全部后移一位,将当前元素置于找到的元素后一位

    如果比较直到首位仍然未发生替换,则说明,该元素是当前最小元素,应该将其置于首位,原有序列需要全部后移一位,来为新的最小元素空位。

5.3 算法实现

 public class InsertSort {

     public static void main(String[] args) {
int[] ints = {2, 6, 4, 9, 12, 98, 5, 32, 90, 33, 24, 65, 37, 12, 4};
sort(ints);
} public static void sort(int[] ints) {
for(int i = 1; i < ints.length; i++) {
if(ints[i] < ints[i - 1]) {
int temp = ints[i];
int j = i - 1;
for(; j >= 0; j--) {
if(temp > ints[j]){
ints[j + 1] = temp;
break;
}else {
ints[j + 1] = ints[j];
}
}
if(j == -1) {
ints[0] = temp;
}
}
}
for(int i : ints) {
System.out.print(i + " ");
}
}
}

5.4 算法解析

  说句实话,每次实现这个算法的时候总会出点幺蛾子,我毕竟没有背代码的习惯,每次都是按照原理来手动实现,总会出现一些问题,总结了一下,其实还是原理理解的有点不透彻。出问题的重点部位就在内循环的实现上。

  外层循环从1开始,即从第二个元素开始循环。表示排第二个元素,依次类推,直到最后一个元素,依次进行排序。

  内存循环的执行还有一个条件,那就是当前元素的值要小于其前一位元素,也就是小于已排序元素的最大值。如果大于最大值,那么新元素就充当新的最大值,置于原位即可。

  内循环从i-1开始,i-1位的元素即为原最大值元素,因为涉及到移位操作,故此处在进行一次比较,i的范围为i-1到0,0位的元素位当前最小值元素,故j>=0。

  如果在内循环过程中找到了比该元素小的已排序元素位j,则将j+1到i-1的元素全部后移一位,将新元素置于j+1位。

  如果再内循环结束之后没能找到比该元素小的以排序元素,则表明该元素是当前的最小元素,需要置于首位(0位),那么从0到i-1位的元素全部都要后移一位来让位。

  代码中我们把内循环的第一句int j = i - 1提取出for循环,在外面定义,目的就是为了在最后循环结束后能借用j的值进行判断是否找到比该元素小的已排序元素,也就是说判断该元素是否为当前最小元素。

  当j最后值为-1的时候,说明j=0的循环也正常执行了,未遭遇到break结束循环,也就是说循环结束后还没找到要替换的位置,也就是说所有已排序元素都大于或等于该元素,那么就将其最后置于首位即可。

  时间复杂度为O(n2)。

  空间复杂度为O(1)。

六、总结

  这四种排序算法都属于简单排序算法,原理很简单,本文中实现方式虽然原始,但是足以说明其原理,当然这些算法都有优化实现方式,喜欢的可以自己试试。

Java基础系列--基础排序算法的更多相关文章

  1. Java多线程系列--“基础篇”11之 生产消费者问题

    概要 本章,会对“生产/消费者问题”进行讨论.涉及到的内容包括:1. 生产/消费者模型2. 生产/消费者实现 转载请注明出处:http://www.cnblogs.com/skywang12345/p ...

  2. Java多线程系列--“基础篇”04之 synchronized关键字

    概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...

  3. Java多线程系列--“基础篇”02之 常用的实现多线程的两种方式

    概要 本章,我们学习“常用的实现多线程的2种方式”:Thread 和 Runnable.之所以说是常用的,是因为通过还可以通过java.util.concurrent包中的线程池来实现多线程.关于线程 ...

  4. Java多线程系列--“基础篇”03之 Thread中start()和run()的区别

    概要 Thread类包含start()和run()方法,它们的区别是什么?本章将对此作出解答.本章内容包括:start() 和 run()的区别说明start() 和 run()的区别示例start( ...

  5. Java多线程系列--“基础篇”05之 线程等待与唤醒

    概要 本章,会对线程等待/唤醒方法进行介绍.涉及到的内容包括:1. wait(), notify(), notifyAll()等方法介绍2. wait()和notify()3. wait(long t ...

  6. Java多线程系列--“基础篇”06之 线程让步

    概要 本章,会对Thread中的线程让步方法yield()进行介绍.涉及到的内容包括:1. yield()介绍2. yield()示例3. yield() 与 wait()的比较 转载请注明出处:ht ...

  7. Java多线程系列--“基础篇”07之 线程休眠

    概要 本章,会对Thread中sleep()方法进行介绍.涉及到的内容包括:1. sleep()介绍2. sleep()示例3. sleep() 与 wait()的比较 转载请注明出处:http:// ...

  8. Java多线程系列--“基础篇”08之 join()

    概要 本章,会对Thread中join()方法进行介绍.涉及到的内容包括:1. join()介绍2. join()源码分析(基于JDK1.7.0_40)3. join()示例 转载请注明出处:http ...

  9. Java多线程系列--“基础篇”09之 interrupt()和线程终止方式

    概要 本章,会对线程的interrupt()中断和终止方式进行介绍.涉及到的内容包括:1. interrupt()说明2. 终止线程的方式2.1 终止处于“阻塞状态”的线程2.2 终止处于“运行状态” ...

  10. Java多线程系列--“基础篇”10之 线程优先级和守护线程

    概要 本章,会对守护线程和线程优先级进行介绍.涉及到的内容包括:1. 线程优先级的介绍2. 线程优先级的示例3. 守护线程的示例 转载请注明出处:http://www.cnblogs.com/skyw ...

随机推荐

  1. DIV与SPAN之间有什么区别

    DIV与SPAN之间有什么区别 DIV 和 SPAN 元素最大的特点是默认都没有对元素内的对象进行任何格式化渲染.主要用于应用样式表(共同点). 两者最明显的区别在于DIV是块元素,而SPAN是行内元 ...

  2. jvm比较详尽的内存结构

     JVM内存结构 2012-09-17 15:27:59 分类: Java 本文转自:http://www.blogjava.net/nkjava/archive/2012/03/14/371831. ...

  3. vfd折腾(一)

    从一开始驱动一块翻出来的液晶显示屏就想做一个电子时钟,偶然翻到了vfd(Vacuum Fluorescent Display的缩写,意为真空荧光显示屏). 此后就走上了不归路

  4. Bootstrap 4,“未捕获错误:Bootstrap工具提示需要Tether(http://github.hubspot.com/tether/)”

    如果出现了这个错误,我想你是没有引用tether文件,这在v4之前不需要单独引入的. https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/te ...

  5. Linux的动态库与静态库

    1.动态库与静态库简介 在实际的软件开发中,为了方便使用一些被重复调用的公共代码,我们经常将这些公共的函数编译成动态库或静态库.我们知道程序一般要经过预处理.编译.汇编和链接这几个步骤才能变成可执行的 ...

  6. 关于SpringMVC控制器的一点补充

    首先复习一下之前控制器的写法:http://www.cnblogs.com/eco-just/p/7882016.html. 我们可以看到,之前的写法是这样的: @RequestMapping(&qu ...

  7. STL-Map 源码剖析

    G++ ,cygnus\cygwin-b20\include\g++\stl_map.h 完整列表 /* * * Copyright (c) 1994 * Hewlett-Packard Compan ...

  8. C#System.Text.RegularExpressions.Regex使用(一) .

    需要引入命名空间 using System.Text.RegularExpressions;(若不引入,则写Regex时要写成 System.Text.RegularExpressions.Regex ...

  9. String 和 new String

    String s1=”welcome”; String s2= “welcome:; String s3 =new String(”welcome”);  //新建对象   指向新的内存   比较内容 ...

  10. js动态计算移动端rem适配问题

    第一:css3的media query来实现适配,例如下面这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 2 ...