public class Bubble {
public static void sort(int []arr){
for (int i = arr.length-1;i > 0;i--){
//每次
for (int j = 0;j < i;j++){
int temp;
//比较,交换顺序
if (arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
import java.util.Arrays;
public class TestSort {
public static void main(String[] args) {
int[] a = {
4,5,9,7,1,3,5};
System.out.println("排序前数组:" + Arrays.toString(a));
Bubble.sort(a);
System.out.println("排序后数组:" + Arrays.toString(a));
}
}
双层for循环,内循环完成排序,故主要分析内循环
最坏情况: 数组完全逆序
O(N^2)
public class Selection {
public static void sort(int[] arr){
for (int i = 0;i < arr.length-2;i++){
//设置arr[i]为最小值所在位置
int minIdex = i;
for (int j = i+1;j < arr.length;j++){
if (arr[minIdex] > arr[j]){
//交换最小值下标
minIdex = j;
}
}
//交换位置
int temp = arr[i];
arr[i] = arr[minIdex];
arr[minIdex] = temp;
}
}
}
双层for循环,外层实现数据交换,内层实现数据比较,分别统计交换次数与比较次数
最坏情况:
O(N^2)
public class Insertion {
public static void sort(int[] a){
for (int i=1;i < a.length;i++){
//当前未排序首位元素为a[i],与之前的元素进行比较,直到找到小于等于a[i]的元素,跳出循环
for (int j=i;j > 0;j--){
if (a[j] < a[j-1]){
int temp = a[j-1];
a[j-1] = a[j];
a[j] = temp;
}else{
//找到最终插入位置,跳出循环
break;
}
}
}
}
}
双层for循环,内层完成排序,主要分析内层循环体执行次数
最坏情况
O(N^2)
上述三种排序时间复杂度都是O(N^2),不适合大规模输入
以下为一些较为高级的算法,争取降低算法时间的最高次幂
又名“缩小增量排序”,插入排序的升级版
增长量h的取值规则:
最大值:
int h = 1
while (h < n/2){
//n排序数组长度
h = 2h + 1;
}
减小:h=h/2
public class Shell {
public static void sort(int[] a){
//数组长度为n
int n = a.length;
//确定增长量h的最大值
int h = 1;
while (h < n/2){
h = 2*h + 1;
}
//开始排序
while (h >= 1){
for (int i=0;i < n;i++){
//a[j]为待插入元素,依次和a[j-h],a[j-2h],a[j-3h]...比较,如果a[j]小,则交换位置,否则a[j]完成插入,跳出循环
for (int j=i;j >= h;j -= h){
if (a[j] < a[j-h]){
int temp = a[j];
a[j] = a[j-h];
a[j-h] = temp;
}else {
break;//跳出循环
}
}
}
h /= 2;//h缩减
}
}
}
因为增长量h没有固定规则,故可以使用事后分析法及逆行分析
经过测试,在处理大量数据时,希尔排序的性能确实优于插入排序
归并排序是建立在归并操作上的一种有效的排序方法,该算法是采用分治法的一个非常典型的例子。
将已有序的子序列合并,得到完全有序的序列(先使子序列有序,再使子序列间有序),将两个子序列表合并成一个有序表,称为二路归并
在定义方法时,在方法内部调用方法本身,为递归
将一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解,大大地减少了程序的代码量
注意: 在递归中不能无限制地调用自己,必须要有边界条件,能够让递归结束。每一次地柜都会在栈内开辟新的空间,重新执行方法,递归的层级太深,很容易造成栈内存溢出。
如:实现二叉树的数据填充方法
通过递归调用add方法,实现左子节点与右子节点的数据插入
public class Node {
//左子节点
public Node leftNode;
//右子节点
public Node rightNode;
//值
public Object value;
//插入数据的方法
public void add(Object v){
//当前节点没有值,就将v赋值给value
if (null == value)
value = v;
else {
if ((Integer) v - ((Integer) value) <= 0){
if (null == leftNode)
leftNode = new Node();
leftNode.add(v); //使用递归
}
else {
if (null == rightNode)
rightNode = new Node();
rightNode.add(v);
}
}
}
package Test;
public class Merge {
//创建辅助数组
private static int[] assist;
/*
排序数组a的元素
*/
public static void sort(int[] a){
assist = new int[a.length];
//创建两个原数组的指针,start:最小索引指针,end:最大索引指针
int start = 0;
int end = a.length - 1;
//对数组a进行拆分,排序,合并
sort(a,start,end);
}
/*
先拆分,后排序合并数组a
*/
private static void sort(int[] a,int start,int end){
//判断,当,end <= start时,说明拆分到最小单位,结束方法
if (end <= start){
return;
}
//定义一个中间指针,实现从中拆分
int middle = start + (end - start)/2;
//递归调用sort进行拆分
sort(a,start,middle);
sort(a,middle+1,end);
//调用merge函数进行归并
merge(a,start,middle,end);
}
/*
排序,合并数组(归并)
*/
private static void merge(int[] a,int start,int middle,int end){
//定义辅助函数插入数据的指针
int i = start;
//创建两个子数组的初始位置指针
int pointer1 = start;
int pointer2 = middle + 1;
//当两个子数组指针都未越界时,两子数组进行比较排序插入辅助数组
while (pointer1 <= middle && pointer2 <= end){
if (a[pointer1] < a[pointer2]){
assist[i++] = a[pointer1++];
}else {
assist[i++] = a[pointer2++];
}
}
//当有一个子数组的指针到了边界,就只进行剩余的数组的数据插入,以下两循环只进行一个
while (pointer1 <= middle){
assist[i++] = a[pointer1++];
}
while (pointer2 <= end){
assist[i++] = a[pointer2++];
}
//当assist数组中所有数据完成排序,就拷贝到原数组
for (int j=start;j <= end;j++){
a[j] = assist[j];
}
}
}
归并排序是分治思想最典型的例子,先将数组进行拆分为(尽量)等长的两部分,分别通过递归调用将它们单独排序,最后将有序的数组归并为最终的排序结果。
该递归的出口在于:如果一个数组不能再被分成两个数组,就执行merge进行归并,在归并的时候比较元素的大小进行排序
复杂度分析:
输入元素个数为n,使用归并排序的拆分次数:log2(n),所以会有log2(n)层树,,故自顶向下第k层有2^k个子数组,每个数组长度 2^(log2(n)-k) ,归并最多需要 2^(log2(n)-k) 次比较,因此每层的比较次数为
2^k*2^(log2(n)-k) = 2^log2(n)
,n层就是log2(n)*2^(log2(n)) = log2(n)*n
O(nlogn)
冒泡排序法的升级版,基本思想:通过一次排序,将要排序的数据分成独立的两个部分,其中一部分的所有数据都比另一部分的所有数据要小,然后重复此步骤对两个部分数据分别进行排序。
public class Quick {
/*
排序总方法
*/
public static void sort(int[] a){
int start = 0;
int end = a.length - 1;
//排序素组所有元素
sort(a,start,end);
}
/*
切分排序
*/
private static void sort(int[] a,int start,int end){
//安全性检测
if (end <= start){
return;
}
//对数组a的start到end的元素进行切分
int partition = partition(a,start,end);
//对左子祖进行排序
sort(a,start,partition - 1);
//对右子组进行排序
sort(a,partition + 1,end);
}
/*
切分
*/
public static int partition(int[] a,int start,int end){
//基准值(切分中间值)
int mid = a[start];
//左指针
int left = start;
//右指针
int right = end + 1;
//进行切分
while (true){
//从右到左扫描,找到比mid小的数值,则停止
while (a[--right] > mid){
//当整个数组没有比mid小的数值时,跳出循环
if (right == left){
break;
}
}
//从左到右扫描,找到比mid大的数值,则停止
while (a[++left] < mid){
//当整个数组没有比mid大的数值时,跳出循环
if (left == end){
break;
}
}
//交换上述两个步骤扫描到的数值的位置
if (left >= right){
//扫描完所有数据
break;
}else {
//交换数组left与right处的值
exch(a,left,right);
}
//right就是切分界限
}
//交换最后right索引处和基准值所在的索引处的值
exch(a,start,right);
return right;
}
private static void exch(int[] a,int i,int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
快速排序的一次切分从两头开始交替搜索,直到left和right重合,因此,一次切分算法的时间复杂度为O(n),但整个快速排序的时间复杂度和切分次数有关
最优情况:
最坏情况:
每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总
共就得切分n次
时间复杂度为O(n^2)
数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保
证A元素依然在B元素的前面,可以说这个该算法是稳定的
如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例
如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第
二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需
要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销
冒泡排序
只有当arr[i]>arr[i+1]的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定的排序
算法
选择排序
选择排序是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 2, 9 },第一遍选择到的最小元素为2,
所以5(1)会和2进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以选择排序是一种不稳定的排序算法
插入排序
比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其
后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等
元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序
是稳定的
希尔排序
希尔排序是按照不同步长对元素进行插入排序 ,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的
归并排序
归并排序在归并的过程中,只有arr[i]
并不会破坏稳定性,归并排序是稳定的
快速排序
快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法