前言
好久没复习基础了,写个冒泡排序都要想一会。感觉自己好像老了好多,今天手痒总结一下排序算法。目前网上博客普遍都有详细介绍,写的很清楚。说实话我是没必要再写一遍的,感觉就是在啰嗦、还是重复性的,但是如果只是单纯看的话,不到3分钟我就忘记了(可能是健忘症晚期)。所以还是自己亲手“教训”一下印象比较深刻。
一、简介
排序(Sorting) 是计算机程序设计中的一种重要操作,它的功能是将一个数据元素(或记录)的任意序列,重新排列成一个关键字有序的序列。
排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
那么怎么对比哪个排序算法更好呢? 这时候就是要看谁运行的比较快。 哈哈 真是一句废话,谁不知道呢
所以我们需要了解一下 算法复杂度。
二、算法复杂度
时间复杂度是指执行算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间
(1)时间复杂度
- 时间复杂度可以认为是对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2)。
- 时间复杂度O(1):算法中语句执行次数为一个常数,则时间复杂度为O(1)。
(2)空间复杂度
- 空间复杂度是指算法在计算机内执行时所需存储空间的度量,它也是问题规模n的函数。
- 空间复杂度O(1):当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)。
- 空间复杂度O(log2N):当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n) , ax=N,则x=logaN。
- 空间复杂度O(n):当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n)。
(3)排序算法稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
稳定性的意义
1、如果只是简单的进行数字的排序,那么稳定性将毫无意义。
2、如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义
3、如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
4、除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。
太复杂了,我也只了解了点皮毛。就简单介绍这些,要详细了解的同学们还是自己看书吧!!!
三、常见算法
(1)冒泡排序
1、简介:
重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来
2、步骤:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
3、动图:
4、代码:
public static void order1(int[] a) {
for(int x=0;xa[y+1]) {
int t = a[y];
a[y] = a[y+1];
a[y+1] = t;
}
}
}
}
(2)选择排序
1、简介:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法
2、步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
3、动图:
4、代码:
public static void order2(int[] a) {
for (int i = 0; i < a.length-1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[i] > a[j]) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
}
}
(3)插入排序
1、简介:
插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。
2、步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到下一位置中
- 重复步骤2~5
3、动图:
4、代码:
public static void order3(int[] a) {
for (int i = 1; i < a.length; i++) {
int get = a[i];
int j = i-1;
while (j >= 0 && a[j] > get) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = get;
}
}
(4)归并排序
1、简介:
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2、步骤:
- 把长度为n的输入序列分成两个长度为n/2的子序列。
- 对这两个子序列分别采用归并排序。
- 将两个排序好的子序列合并成一个最终的排序序列。
3、动图:
4、代码:
/**
* 归并排序
*/
public static void order4(int[] a) {
sort(a,0,a.length-1);
}
public static void sort(int[] data,int left,int right) {
if(left >= right) return;
//找出中间索引
int center = (left + right)/2;
//对左边数组进行递归
sort(data, left, center);
//对右边数组进行递归
sort(data, center+1, right);
//合并
merge(data,left,center,right);
}
public static void merge(int[] data,int left, int center,int right) {
//临时数组
int[] tmpArr = new int[data.length];
//右数组第一个元素索引
int mid = center + 1;
//third 记录临时数组的索引
int third = left;
//缓存左数组第一个元素的索引
int tmp = left;
while (left <= center && mid <=right) {
//从两个数组中取出最小的放入临时数组
if (data[left] <= data[mid]) {
tmpArr[third++] = data[left++];
} else {
tmpArr[third++] = data[mid++];
}
}
//剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while(mid <= right) {
tmpArr[third++] = data[mid++];
}
while(left <= center) {
tmpArr[third++] = data[left++];
}
//将临时数组中的内容拷贝回原数组中
while (tmp <= right) {
data[tmp] = tmpArr[tmp++];
}
}
(5)快速排序
1、简介:
在数组中随机选一个数(默认数组首个元素),数组中小于等于此数的放在左边,大于此数的放在右边,再对数组两边递归调用快速排序,重复这个过程。
2、步骤:
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
3、动图:
4、代码:
/**
* 快速排序
*/
public static void order5(int[] a) {
quickSort(a,0,a.length-1);
}
private static void quickSort(int[] a, int left, int right) {
if(left < right) {
int i = getMiddle(a,left,right);
quickSort(a, left, i - 1);
quickSort(a, i + 1,right);
}
}
private static int getMiddle(int[] a, int low, int high) {
int pivot = a[low];
int i = low;
int j = high;
while(i < j) {
while(pivot <= a[j] && i < j) j--;
while(pivot >= a[i] && i < j) i++;
if(i < j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[low] = a[i];
a[i] = pivot;
return i;
}
(6)希尔排序
1、简介:
希尔排序是插入排序改良的算法,希尔排序步长从大到小调整,第一次循环后面元素逐个和前面元素按间隔步长进行比较并交换,直至步长为1,步长选择是关键。
2、步骤:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
3、动图:
4、代码:
/**
* 希尔排序
*/
public static void order6(int[] a) {
int len = a.length;//单独把数组长度拿出来,提高效率。
while(len != 0) {
len = len/2;
for (int i = 0; i < len; i++) {//分组
for (int j = i + 1; j < a.length; j+=len) {//元素从第二个开始
int k = j - len;//k为有序序列最后一位的位数
int temp = a[j];//要插入的元素
while (k >= 0 && temp < a[k]) {//从后往前遍历
a[k + len] = a[k];
k -= len;//向后移动len位
}
a[k + len] = temp;
}
}
}
}
(7)基数排序
1、简介:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
2、步骤:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
3、动图:
4、代码:
/**
* 基数排序
*/
public static void order7(int[] a) {
int max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max) {
max = a[i];
}
}
int time = 0;
while (max > 0) {
max /= 10;
time++;
}
List> queue = new ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList queue1 = new ArrayList();
queue.add(queue1);
}
for (int i = 0; i < time; i++) {
for (int j = 0; j < a.length; j++) {
int x = a[j] % (int) Math.pow(10, i+1)/(int)Math.pow(10, i);
ArrayList queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
int count = 0;
for (int k = 0; k < 10; k++) {
while (queue.get(k).size()>0) {
ArrayList queue3 = queue.get(k);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
(8)堆排序
1、简介:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
2、步骤:
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
3、动图:
4、代码:
/**
* 堆排序
*/
public static void order8(int[] a) {
int len = a.length;
for (int i = 0; i < len - 1; i++) {
buildMaxHeap(a,len - 1 - i);
swap(a,0,len - 1 - i);
}
}
private static void buildMaxHeap(int[] data, int lastIndex) {
//从lastIndex处节点(最后一个节点)的父节点开始
for (int i = (lastIndex - 1)/2; i >= 0; i--) {
//k保存正在判断的节点
int k = i ;
//如果当前K节点的子节点存在
while(k * 2 + 1 <= lastIndex) {
//k节点的左子节点的索引
int biggerIndex = 2 * k +1;
//如果biggerIndex小于lastIndex,即biggerIndex +1代表的K节点的右子节点存在
if(biggerIndex < lastIndex) {
//如果右子节点的值较大
if(data[biggerIndex] < data[biggerIndex + 1]) {
biggerIndex++;
}
}
//如果K节点的值小于其较大的子节点的值
if(data[k] < data[biggerIndex]) {
//交换他们
swap(data, k, biggerIndex);
k = biggerIndex;
} else {
break;
}
}
}
}
private static void swap(int[] a, int i, int j) {
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
四、总结
场景应用:
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
果然算法很复杂,以我智商都有点不够用。分析的我脑壳有点疼,实在扯不下去。就写这么多了。见谅。。。
五、Demo地址
https://github.com/DayorNight/JavaOrderingMethod
六、参考文档
《Java数据结构与算法》
http://www.runoob.com/w3cnote/sort-algorithm-summary.html
https://jingyan.baidu.com/article/db55b609f856604ba30a2f18.html
https://blog.csdn.net/yushiyi6453/article/details/76407640
http://www.cnblogs.com/aoyihuashao/p/9453649.html
https://www.cnblogs.com/end/archive/2011/10/22/2220995.html
七、内容推荐
CSDN:https://blog.csdn.net/cs_lwb/article/details/84556693
上一篇:
《Java 设计模式总结》
《JAVA 设计模式——单例模式》
《Java 设计模式——工厂模式》
《Java 设计模式——观察者模式》
《Java 设计模式——建造者模式》
如果你觉得我写的不错或者对您有所帮助的话
不妨顶一个【微笑】,别忘了点赞、收藏、加关注哈
您的每个举动都是对我莫大的支持