Pre-Comparable接口
java提供了一个接口Comparable 就是用来定义排序规则,接口中有一个抽象方法,实现比较规则。
package comparable;
/*
定义测试类Test,在测试类Test中定义测试方法Comparable getMax(Comparable c1, Comparable c2)完成测试
*/
public class Comparable1 {
public static void main(String[] args) {
student s1 = new student("张三",18);
student s2 = new student("李四",20);
Comparable max = getMax(s1,s2);
System.out.println(max);
}
public static Comparable getMax(Comparable c1,Comparable c2){ // 相当于多态 左侧是接口,右侧是实现类对象
int result = c1.compareTo(c2);
if (result>=0){
return c1;
}else{
return c2;
}
}
}
package comparable;
import javax.print.DocFlavor;
/*
定义一个学生类student,具有年龄和姓名username两个属性,并通过Comparable接口提供比较规则
*/
public class student implements Comparable<student>{
private String name;
private int age;
public student() {
}
public student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(student o) {
return this.getAge()-o.getAge();
}
}
一、简单排序
1. 冒泡排序
1.1 代码实现
package bubble;
import java.util.Arrays;
public class BubbleTest {
public static void main(String[] args) {
Integer[] arr = {4,5,6,3,2,1};
Bubble1.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
package bubble;
import java.util.Comparator;
public class Bubble1 {
/*
对数组中的元素进行排序
*/
public static void sort(Comparable[] a ){
for (int i=a.length-1;i>0;i--){ //确定每次参与冒泡元素的个数,每次都将最大的元素放到最大索引处,所以每次都减1
for (int j=0;j<i;j++){ //每次比较相邻两个元素的大小,进行交换位置
//比较索引j和索引j+1处的值
if (greater(a[j],a[j+1])){
exch(a,j,j+1);
}
}
}
}
/*
比较元素是否大于w元素
*/
private static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
1.2 复杂度分析
冒泡循环使用了双层for循环,其中内层的循环体是真正完成排序的代码,所以,我们分析冒泡排序的时间复杂度,主要分析一下内层循环体的执行次数即可。
在最坏的情况下,要排序的元素为{6,5,4,3,2,1} ,那么:
元素比较的次数为:
(N-1)+(N-2)+(N-3)+…+2+1 = ((N-1)+1)(N-1)/2=N^2/2-N/2
元素交换的次数为:
(N-1)+(N-2)+(N-3)+…+2+1 = ((N-1)+1)(N-1)/2=N^2/2-N/2
总执行次数:
N ^ 2 /2-N/2 + N^2/2-N/2 = N ^ 2 -N
大O推导法则: 时间复杂度为 O(N^2)
2. 选择排序
2.1 排序原理
- 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引处的值为最小值,最后可以找到最小值所在的索引
- 交换第一个索引和最小值所在的索引处的值
2.2 代码实现
- 选择最小值
package select;
import java.util.Arrays;
public class SelectTest {
public static void main(String[] args) {
//原始数据
Integer[] a = {4,6,8,7,9,2,10,1};
Select1.sort(a);
System.out.println(Arrays.toString(a));
}
}
package select;
public class Select1 {
public static void sort(Comparable[] a){
for(int i=0;i<a.length-2;i++){
int minIndex = i;
for (int j=i+1;j<a.length;j++){
//需要比较最小索引minIndex处的值和j索引处的值
if (greater(a[minIndex],a[j])){
minIndex=j;
}
}
//交换最小元素所在索引minIndex处的值和索引i处的值
exch(a,i,minIndex);
}
}
/*
比较元素是否大于w元素
*/
private static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
- 选择最大值
package sort;
public class Select {
private static Comparable[] assist;
public static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void sort(Comparable[] a){
assist = new Comparable[a.length];
for (int i=a.length-1; i>0; i--){
int maxindex = i;
for (int j=0; j<i; j++){
if (greater(a[j],a[maxindex])){
maxindex = j;
}
}
exch(a,i,maxindex);
}
}
}
package test;
import sort.Select;
import java.util.Arrays;
public class SelectTest {
public static void main(String[] args) {
Integer[] a = {4,5,6,3,2,1};
Select.sort(a);
System.out.println(Arrays.toString(a));
}
}
2.3 复杂度分析
选择排序使用了双层for循环,其中外层循环完成了数据交换,内层循环完成了数据比较,所以我们分别统计数据交换次数和数据比较次数:
数据比较次数:
(N-1)+(N-2)+(N-3)+…+2+1 = ((N-1)+1)*(N-1)/2=N^2/2-N/2
数据交换次数:
N-1
时间复杂度:
N^2/2-N/2+N-1
大O复杂度:O(N^2)
3. 插入排序
3.1 排序原理
- 把所有的元素分成两组,已排序的和未排序的
- 找到未排序的组中的第一个元素,向已经排序的组中进行插入
- 倒叙遍历已经排序的元素,依此和插入的元素进行比较,直到找到一个元素小于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位
3.2 代码实现
package insertion;
import java.util.Arrays;
public class InnsertionTest {
public static void main(String[] args) {
Integer[] a = {4,3,2,10,12,1,5,6};
Insertion1.sort(a);
System.out.println(Arrays.toString(a));
}
}
package insertion;
public class Insertion1 {
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a)
{
for(int i=1;i<a.length;i++){
for(int j=i;j>0;j--){
//比较索引处的值和索引-1处的值,如果索引j-1处的值比索引j处的值大,如果不大,那么就找到合适的位置了,退出循环即可
if (greater(a[j-1],a[j])){
exch(a,j-1,j);
}else{
break;
}
}
}
}
/*
比较元素是否大于w元素
*/
private static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
3.3 时间复杂度分析
最坏情况:[12, 10, 6, 5, 3 ,2 ,1]
比较次数:
(N-1)+(N-2)+(N-3)+…+2+1 = ((N-1)+1)*(N-1)/2=N^2/2-N/2
交换次数:
(N-1)+(N-2)+(N-3)+…+2+1 = ((N-1)+1)*(N-1)/2=N^2/2-N/2
总执行次数:
N ^ 2 /2-N/2 + N^2/2-N/2 = N ^ 2 -N
大O推导法则:O(N^2)
二、高级排序
简单排序的复杂度都是O(N^2),不适应大规模输入
1.希尔排序
1.1 排序原理
- 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组
- 对分好组的每一组数据完成插入排序
- 减小增长量,最小减为1,重复第二步操作
- h的确定
- 理解
希尔排序就是利用不同的h,对原数组进行不同形式的分组。对于每个h,原数组都可以分为几个不同的组,每个组的起点是从第h各索引处依此递增,一直到length-1索引处。
int h = 1;
while(h<length/2){
h = 2*h+1;
}
//h的减小规则
h = h/2;
1.2 代码实现
package shell;
import java.util.Arrays;
public class ShellTest {
public static void main(String[] args) {
Integer[] a = {9,1,2,5,7,4,8,6,3,5};
Shell1.sort(a);
System.out.println(Arrays.toString(a));
}
}
package shell;
public class Shell1 {
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
//确定h值
int h = 1;
while(h<a.length/2){
h=2*h+1;
}
//希尔排序
while(h>=1){
//确定待排序的元素
for(int i=h;i<a.length;i++){
//对待排序的元素在当前的分组下进行插入排序
/*
最开始以为下面的循环没有用,直接用i和i-h索引处的值进行判断不就行了?
这样只是将i处和i-h处的进行交换了,i和i-2h处的值就没有比较交换了
所以这个内层循环就是控制分组的,当确定i和h时,每次执行这个内层的循环就是执行【每个i对应的组别】的插入排序
因为涉及a[j]和a[j-h]的比较,所以j>=h
*/
for (int j = i; j>=h;j-=h){
if (greater(a[j-h],a[j])){
exch(a,j-h,j);
}else{
break;
}
}
}
//减小h的值
h = h/2;
}
}
/*
比较元素是否大于w元素
*/
private static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
1.3 时间复杂度分析
事先分析,比较复杂。采用事后复杂。
2. 归并排序
2.1 排序原理
- 尽可能的将一组数据分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
- 将相邻的两个子组进行合并成一个有序的大组
- 不断重复步骤2,知道最终只有1个组为止
- 归并排序的原理:
2.2 代码实现
package merge;
public class Merge1 {
//归并所需要的辅助数组
private static Comparable[] assist;
/*
比较元素是否小于w元素
*/
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w)<0;
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
//初始化辅助数组assist
assist = new Comparable[a.length];
//定义一个lo变量,和hi变量,分别记录数组中最小索引和最大索引
int lo = 0;
int hi = a.length-1;
//调用sort重载方法完成数组a中,从索引lo到hi索引的元素排序
sort(a,lo,hi);
}
/*
对数组a中从lo到hi的元素进行排序
*/
private static void sort(Comparable[] a, int lo, int hi){
//做安全性检测,保证迭代能结束
if(hi<=lo){
return;
}
//对lo到hi之间的数据进行分为两个组
int mid = lo+(hi-lo)/2;
//分别对每一组的数据进行排序
/*
这个sort进行递归,对一个数组不停的进行分割,一直分割到最小的的子组,开始返回,
返回到倒数第二层的子组,然后将倒数第二层的子组的右子组分割出出来(1个元素),
继续返回到倒数第二层的子组,此时进行归并排序。倒数第二层排序结束以后,再向上返回,一次类推,完成排序。
*/
sort(a, lo, mid);
sort(a,mid+1,hi);
merge(a,lo,mid,hi);
}
//再把两个组中的数据进行归并(归并的过程中进行排序)
private static void merge(Comparable[] a, int lo, int mid, int hi){
//定义3个指针
int i = lo;
int p1 = lo;
int p2 = mid+1;
//遍历,移动指针p1和p2,比较对应处的的值,找出最小的那个,放到辅助数组的对应索引处
while(p1<=mid && p2<=hi){
if (less(a[p1],a[p2])){
assist[i++] = a[p1++];
}else{
assist[i++] = a[p2++];
}
}
//遍历,如果p1指针没有走完,那么顺序移动p1的指针,把对应的元素放到辅助数组的索引处
while (p1<=mid){
assist[i++] = a[p1++];
}
//遍历,如果p2指针没有走完,那么顺序移动p1的指针,把对应的元素放到辅助数组的索引处
while (p2<=hi){
assist[i++] = a[p2++];
}
//把辅助数组中的元素拷贝到原数组中
for (int index = lo; index<=hi; index++){
a[index] = assist[index];
}
}
}
package merge;
import java.lang.reflect.Array;
import java.util.Arrays;
public class MergeTEst {
public static void main(String[] args) {
Integer[] a = {66,9,8,4,5,7,1,3,6,2};
Merge1.sort(a);
System.out.println(Arrays.toString(a));
}
}
2.3复杂度分析
不考虑递归,就i看二叉树的每一层,对于每一层的子数组,最坏的比较情况就是每个子数组的指针遍历了该数组的所有元素,也就是n(比较了n次);而二叉树的深度为 l o g 2 n log_2^n log2n层,所以归并排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
3.快速排序
3.1 排序原理
- 首先设定一个分界值,通过该分界值将数组分成左右两部分
- 将大于或等于分界值得数据放到数组的右边,小于分界值的数组放到数组的左边,此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值
- 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小的值,右边放置较大的值。右侧的数组数据也可以做类似处理。
- 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排序好,再递归排好右侧部分的顺序。当左侧和右侧两部分数据排序完成后,整个数组也就排序完成了。
切分原理
2.2 代码实现
package quick;
public class Quick1 {
/*
比较v元素是否小于w元素
*/
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w)<0;
}
/*
数据元素i和数据元素j进行交换
*/
private static void exch(Comparable[] a, int i, int j){
Comparable t=a[i];
a[i] = a[j];
a[j] = t;
}
/*
对数组内的元素进行排序
*/
public static void sort(Comparable[] a){
int lo = 0;
int hi = a.length-1;
sort(a,lo,hi);
}
/*
对数组a中的元素从索引lo到索引hi之间的元素进行排序
*/
private static void sort(Comparable[] a, int lo, int hi){
//安全性检测
if (lo>=hi){
return;
}
//进行切分,获得切分处的索引
int partition = partition(a,lo,hi);
//对获得的左子组进行继续切分分组
sort(a,lo,partition-1);
sort(a,partition+1,hi);
}
/*
对数组a中,从索引lo到索引hi之间的元素进行分组,并返回分组界限对应的索引
*/
public static int partition(Comparable[] a ,int lo, int hi){
int left = lo;
int right = hi+1;
int key = lo;
while (true){
//右侧指针向左移动,找到比key小的值停止
while (less(a[key],a[--right])){
if (right==lo){
break;
}
}
//左侧指针向右移动,找到比key大的值停止
while (less(a[++left],a[key])){
if (left==hi){
break;
}
}
if (right<=left){
break;
}else{
exch(a,right,left);
}
}
//将右指针对应的元素和key交换(右指针指向小于key处的值)
exch(a,lo,right);
return right;
}
}
package quick;
import java.util.Arrays;
public class QuickTest {
public static void main(String[] args) {
Integer[] a = {6,1,2,7,9,4,4,5,8};
Quick1.sort(a);
System.out.println(Arrays.toString(a));
}
}
2.3 复杂度分析
-
快速排序和归并排序的区别:
- 归并排序将数组分成两个子数组分别排序,并将有序的组数组归并从而将整个数组排序;而快速排序的方式则是当两个数组都有序时,整个数组自然就有序了
- 在归并排序中,一个数组被分两半,归并调用发生在处理整个数组之前,而在快速排序中,切分数组的位置取决于数组的内容,递归调用发生在处理整个数组之后。(一个是先切分后排序,一个是先排序,后切分)
-
快速排序复杂度分析
快速排序的一次切分从两头开始,直到和right重合,因此,一次切分算法的时间复杂度为O(n),但整个快速排序的时间复杂度和切分次数相关。-
最优情况:
每一次切分选择的基准数字刚好将当前序列等分:O(nlogn)
-
最坏情况:
每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得【每次切分都会有一个子组】,那么总共就得切分n次,所以,最坏情况下,快速排序的时间复杂度为O(n^2)
-
-
平均情况:数学归纳法:O(nlogn)
排序的稳定性
-
排序的稳定性:
数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法后,能保证A元素依然能在B元素的前面,可以说这个算法是稳定的。 -
稳定性的意义:
多次排序就需要选择稳定性的算法 -
排序稳定性分析
排序核心部分如果遇到【大交换】就会出现不稳定的情况。【大交换】即排序过程中,数据不连续插入有序数组中。- 冒泡排序稳定性:稳定
- 选择排序:不稳定,(直接选区最小值,找到无序数组中的最小值,和最小索引处值交换)
{5(1),8,5(2),2,9} 5(1)和2比较时,2被放到第一位,5(1)放在原来2的位置,这样5(1)和5(2)就交换了位置 - 插入排序:稳定
- 希尔排序:不稳定:在分组的过程中,不是连续插入有序数组,有间隔
- 归并排序:稳定
- 快速排序:不稳定:排序时,没有连续的插入有序数组,交换的方式。