排序算法关系如下:
性能比较如下:
一、直接插入排序
插入排序是一种最简单直观的排序算法, 它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
1)将第一待排序列的第一个元素看成一个有序序列,把第二个元素到最后一个元素看作是未排序序列。
2)从头到尾依次扫描未排序序列(通过外层循环),将扫描到的每个元素插入到有序序列的适当位置(通过内层循环)。(如果待插入元素和有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
代码实现:
package web;
public class InsertSort {
public static int[] sort(int[] ins) {
for(int i = 1;i<ins.length;i++) { //从头到尾依次扫描待排序列(将第一个元素ins[0]看成有序的,所以待排序列从ins[1]开始扫描)
for(int j = i;j>0 && ins[j]<ins[j-1];j--) { //操作当前待排元素:将当前待排元素与前面已经排好的有序序列做比较(比较方法是依次与前一个元素比较,若比前一个元素小就交换位置),插入到正确位置
//if(ins[j]<ins[j-1]) {
int tmp = ins[j-1];
ins[j-1] = ins[j];
ins[j] = tmp;
//}
}
}
return ins;
}
//我们可以吧所有的大于需要插入的数先保存,然后进行比较,然后将最后的正确位置空出来。吧之前保存的需要插入的数放到正确位置上;
public static int[] sort2(int[] ins){
for(int i=1; i<ins.length; i++){
int temp = ins[i];//保存每次需要插入的那个数
int j;
for(j=i; j>0&&ins[j-1]>temp; j--){//这个较上面有一定的优化
ins[j] = ins[j-1];//吧大于需要插入的数往后移动。最后不大于temp的数就空出来j
}
ins[j] = temp;//将需要插入的数放入这个位置
}
return ins;
}
public static void main(String[] args) {
int[] ins = {2,3,5,1,23,6,78,34};
System.out.print("待排序列:");
for(int i : ins) {
System.out.print( i + " ");
}
System.out.println();
int[] ins2 = sort(ins);
System.out.print("有序序列(方法一):");
for(int i : ins2) {
System.out.print( i + " ");
}
System.out.println();
int[] ins3 = sort(ins);
System.out.print("有序序列(方法二):");
for(int i : ins3) {
System.out.print( i + " ");
}
}
}
二、希尔排序
希尔排序是插入排序的一种,又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本,它的实质是一种分组插入方法。希尔排序是非稳定排序算法。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减少至1时,整个文件恰好被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1、插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
2、但是插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
代码实现:
希尔排序不稳定,原因:
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
代码实现:
package web;
public class ShellSort {
public static void main(String[] args) {
int[] ins = {12,34,1,0,56,34,2,45,78,10,89,2,0};
for(int i : ins) {
System.out.print(i + " ");
}
System.out.println();
int[] ins2 = sort1(ins);
for(int i : ins2) {
System.out.print(i + " ");
}
}
//先分组,内层用了直接选择排序(不是希尔的思想,但是可以实现)
private static int[] sort(int[] ins) {
int a = ins.length/2;
while(a > 0) {//控制分组的步长
for(int i = 0;i < a;i++) { //外层控制分组
for(int j = i;j<ins.length;j=j+a) {//对每一小组中的每个待排序元素进行遍历,执行直接选择排序
int min = ins[j];
int minflag = j;
for(int k=j+a;k<ins.length;k=k+a) {// 遍历出最小的来进行保存
if(ins[k]<min) {
min = ins[k];
minflag = k;
}
}
int tmp = ins[j];//将一趟遍历后的最小元素替换到当前待排序元素处
ins[j]= min;
ins[minflag] = tmp;
}
}
a/=2;
}
return ins;
}
//希尔先分组,内层用了直接插入排序
private static int[] sort1(int[] ins) {
int a = ins.length/2;
while(a > 0) {//控制分组的步长
for(int i = 0;i < a;i++) { //外层控制分组
for(int j = i+a;j<ins.length;j=j+a) {//对当前分组执行直接插入排序
for(int k = j;k>i;k=k-a) {//前边是排好序的,将当前元素向前找位置插入
if(ins[k]<ins[k-a]) {
int tmp = ins[k-a];
ins[k-a]= ins[k];
ins[k] = tmp;
}
}
}
}
a/=2;
}
return ins;
}
}
三、直接选择排序
选择排序也是一种简单直观的排序算法。
算法步骤:
1)首先在未排序序列中找到最小元素,存到排序序列的起始位置。
2)再从剩余未排序序列中寻找最小元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
算法实现:
package web;
public class SelectSort {
public static int[] sort(int[] ins) {
for(int i = 0;i < ins.length;i++) { //对数组中每一个未排序序列的初始元素执行选择排序操作
//min 记录当前未排序序列的首元素的值,x记录其下标
int min = ins[i];
int x = i;
for(int j = i + 1;j<ins.length;j++) { //对当前未排序序列进行比较遍历,找出最小的值和下标记录下来
if(ins[j] < min) {
min = ins[j];
x = j;
}
}
//将上面遍历过程记录下来的值和下标进行替换,即完成当前这一趟的排序
int tmp = ins[i];
ins[i]= min;
ins[x] = tmp;
//System.out.print("第"+i+"趟 ");
//for(int m : ins) {
// System.out.print( m + " ");
//}
//System.out.println();
}
return ins;
}
public static void main(String[] args) {
int[] ins = {12,34,1,0,56,34,2,45,78,10,89,2,0};
for(int i : ins) {
System.out.print(i + " ");
}
System.out.println();
int[] ins2 = sort(ins);
for(int i : ins2) {
System.out.print(i + " ");
}
}
}
直接选择排序不稳定,原因:
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的。依此类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下他一个最大的元素了。那么在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举例如下:5 8 5 2 9,我们知道第一遍选择第一个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
四、堆排序
堆排序是利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子节点的键值或索引总是小于(或大于)它的父节点。
堆排序分析:
毫无疑问,排序两个字没必要去死磕,这里的重点在于排序的方式,堆排序,就是以堆的形式去排序,所以了解堆很重要。
那么,什么是堆呢?
这里必须引入一个完全二叉树的概念,然后过渡到堆的概念。
上图就是一个完全二叉树,其特点在于:
1、从作为第一层的根开始,除了最后一层之外,第n层的元素个数都必须是2的n次方;第一层2个元素,第二层4个,第三层8个,以此类推。
2、最后一层的元素,都要紧贴在左边。换句话说,每一行的元素都从最左边开始安放,两个元素之间不能有空闲。
具备了上述两个特点的树,就是一颗完全二叉树。
那么,完全二叉树与堆有什么关系呢?
我们假设有一颗完全二叉树,在满足作为完全二叉树的基础上,对于任意一个拥有父节点的子节点,其数值均不小于父节点的值,这样层层递推,就是跟节点的值最小,这样的树,称为小根堆。
同理,又有一棵完全二叉树,对于任意一个子节点来说,均不大于其父节点的值,如此递推,就是根节点的值是最大的,这样的树称为大根堆。
如上图,左边是大根堆,右边是小根堆。这里需要注意一点,只要求子节点与父节点的关系,两个节点的大小关系与其左右位置没有任何关系。
下面说堆排序:
现在,对于堆排序来说,我们要先做的是,把待排序的一堆无序的数,整理成一个大根堆或者小根堆,下面讨论以大根堆为例子。
给定一个列表array=[16,7,3,20,17,8],对其进行堆排序(使用大根堆)。
步骤一 构造初始堆。将给定的无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下:
b.此时我们从最后一个非叶子节点开始(叶子节点自然不用调整,第一个非叶子节点arr.length/2-1=5/2=1=1,也就是下面的6节点),从左至右,从下至上进行调整。
此处需要注意,我们把6和9交换之后,必须考量9这个节点对于其子节点会不会产生影响呢?因为其是叶子节点,所以不加考虑;但是,一定要熟练这种思维,写代码的时候就比较容易理解为什么会出现一次非常重要的交换了。
c.找到第二个非叶子节点4,由于[4,9,8]中9元素最大,4和9交换。
在真正的代码实现中,这是4和9交换之后,必须考虑9所在的这个节点位置,因为其上的值变了,必须判断其对于它的两个子节点是否造成了影响,实际上就是判断其作为根结点的那棵子树,是否还满足大根堆的原则,每一次交换都必须要循环把自树部分判别清楚。
这时,交换导致了子根[4,5,6]关系混乱,继续调整,[4,5,6]中6最大,交换4和6。
牢记上面说的规则,每次交换都要把改变了的那个节点所在的树重新判定一下,这里就用上了,4和9交换了,变动了的那棵子树就必须重新调整,一直调整到符合大根堆的规则为止。
此时,我们就把一个无序序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9与末尾元素4进行交换。
这里需要说明一下,所谓的交换,实际上就是把最大值从树里面拿掉了,剩下参与到排序中的树,其实只有总节点的个数减去拿掉的节点的个数了。所以图中用的是虚线。
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8。
d.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。
代码实现:
package com;
import java.util.Arrays;
public class HeapSort {
public static void main(String []args){
int []arr = {3,1,4,2,8,5,9,7,6};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
五、冒泡排序
它重复的走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序错误就把他们较缓过来。走访元素的工作是重复的进行直到没有相邻元素需要交换,也就是说该元素序列已经排序完成。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
基本思想:
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
package web;
public class maopaoSort {
public static void main(String[] args) {
int[] ins = {2,3,5,1,23,6,78,34};
int[] ins2 = sort(ins);
for(int in: ins2){
System.out.print(in + " ");
}
}
private static int[] sort(int[] ins) {
for(int i = 0; i<ins.length-1;i++) {//ins.length-1是需要比较的回合数
for(int j =0;j < ins.length-i-1;j++) {
if(ins[j] > ins[j+1] ) {
int tmp = ins[j+1];
ins[j+1] = ins[j];
ins[j] = tmp;
}
}
}
return ins;
}
}
六、快速排序
基本思想:
1、先从数列中取出一个数作为基准数。
2、分区过程,将比这个数大的数全都放到它的右边,小于它的数全都放到它左边。
3、再对左右分区重复第二步,直到各区间只有一个数。
理解:挖坑填数+分治法
举例如下:
以一个数组作为示例,取区间第一个数为基准数。
初始时,i = 0; j = 9; X = a[i] = 72
由于已经将 a[0] 中的数保存到 X 中,可以理解成在数组 a[0] 上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;
数组变为:
i = 3; j = 7; X=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结:
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
代码实现:
package sort;
public class QuickSort {
public static int[] sort(int[] ins,int start,int end) {
if(start>end) {
return ins;
}
int mid = ins[start];
int low = start;
int high = end;
while(low < high) {
while(low < high && ins[high] >= mid) {
high--;
}
ins[low] = ins[high];
while(low < high && ins[low] < mid) {
low ++;
}
ins[high] = ins[low];
}
ins[low] = mid;
sort(ins,start,low-1);
sort(ins, low+1, end);
return ins;
}
public static void main(String[] args) {
int[] ins = {2,3,5,1,34,88,56,23};
int[] ins2 = sort(ins, 0, ins.length-1);
for(int i : ins2) {
System.out.print(i + " ");
}
}
}
七、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,是分治法的一个非常典型的应用。将已有序的自序列合并,得到完全有序的序列;即先使每个自序列有序,再使自序列段间有序,若将两个有序表合并成一个有序表,称为二路归并。
算法描述:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置。
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
重复步骤3直到某一指针超出序列尾。
将另一序列剩下的所有元素直接复制到合并序列尾部。
稳定性:
归并排序为稳定排序算法(即相等元素的顺序不会改变),速度仅次于快速排序,归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。
代码实现:
package web;
public class MergeSort {
public static void main(String[] args) {
int[] ins = {12,34,1,0,56,34,2,45,78,10,89,2,0};
for(int i : ins) {
System.out.print(i + " ");
}
System.out.println();
int[] ins2 = sort(ins,0,ins.length-1);
for(int i : ins2) {
System.out.print(i + " ");
}
}
private static int[] sort(int[] ins,int l,int h) {
if(l == h) {
return new int[]{ins[l]};
}
int mid = l + (h - l) / 2;
int[] leftArr = sort(ins, l, mid);//左有序数组
int[] rightArr = sort(ins, mid+1, h);//右有序数组
int[] newIns = new int[leftArr.length + rightArr.length];//新有序数组
int m = 0, i = 0, j = 0;
while(i < leftArr.length && j < rightArr.length) {
newIns[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : rightArr[j++];
}
while(i < leftArr.length) {
newIns[m++] = leftArr[i++];
}
while(j < rightArr.length) {
newIns[m++] = rightArr[j++];
}
return newIns;
}
}
八、基数排序
基数排序是通过“分配”和“收集”两个过程来实现排序的。
举例如下:
(1)有以下待排序列
309 385 867 183 341 605 385
(2)第一次分配和收集:
首先按照各个数据的个位数字(即按照9,5,7,3,1,5,5)分配到0-9的10个区间内
结果如下:
分配结束后。接下来将所有空间中的数据按照序号由小到大依次重新收集起来,得到如下仍然无序的数据序列:
(3)第二次分配和收集:
这次按照十位数字进行分配,步骤同上
结果如下:
分配结束后。接下来将所有空间中的数据按照序号由小到大依次再次收集起来,得到如下仍然无序的数据序列:
(4)第三次分配和收集:
这次按照百位数字进行分配,步骤同上
结果如下:
分配结束后。接下来将所有空间中的数据按照序号由小到大依次再次收集起来;
数据排序完成。
代码实现:
package sort;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
//基数排序
public class RadixSort1 {
public static void main(String[] args) {
int[] a = {250,340,520,25,7,63,55,49,0};
//获取最大的值
int max = Arrays.stream(a).max().getAsInt();
//获取最大的位数
int i = (max + "").length();
for(int n = 1; n <= i; n++) {
List<Integer>[] list = new ArrayList[10];
for(int m = 0; m < 10; m++) {
list[m] = new ArrayList<>();
}
for(int num : a) {
int bit = getBit(num,n);
list[bit].add(num);
}
list2array(list, a);
}
for(int x : a) {
System.out.print(x + " ");
}
}
//对每个数获取某一位的值
private static int getBit(int num, int n) {
return (int)(num/Math.pow(10, n-1)%10);
}
//每轮排完之后重新调整a数组
public static void list2array(List<Integer>[] lists , int[] a) {
int i = 0;
for(List<Integer> integers : lists) {
for(int m = 0;m<integers.size() && i < a.length;m++,i++) {
a[i] = integers.get(m);
//i++;
}
}
}
}