Java数据结构与算法-排序

Java数据结构与算法

近期学习了些数据结构与算法的内容,对于笔记做一个记录,方便以后回看修改
学习资料全部来源于网络视频,这章先记录关于排序的实现

基本数据结构

数据结构分为"线性结构""非线性结构"
逻辑结构分类:
"集合结构":结构中的数据元素"除了同属于一种类型外,别无其它关系"
"线性结构":结构中的数据元素之间存在"一对一"的关系
"树型结构" :结构中的数据元素之间存在"一对多"的关系
"图状结构或网状结构":结构中的数据元素之间存在"多对多"的关系

物理结构分类: 
"顺序存储结构":用数据元素在存储器中的相对位置来表示数据元素之间的逻辑关系。 
"链式存储结构":在每一个数据元素中增加一个存放地址的指针,用此指针来表示数据元素之间的逻辑关系

算法和时间复杂度:
1.算法函数中的常数可以忽略
2.算法函数中最高次幂的常数因子可以忽略
3.算法函数中最高次幂越小,算法效率越高

"执行次数=执行时间"
一般使用大O表示法
    1.用常数1取代运行时间中所有加法常数   例如 3次 记作O(1)
    2.在修改后的运行次数中,只保留高阶项   例如: n^2+n次 记作 O(n^2)
    3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数             
    								 例如: 2n^2+3次  记作 O(n^2)
"时间复杂度比较"
O(1) < O(logn) < O(n)< O(nlogn)< O(n^2)< O(n!)< O(n^n)

代码中的空间

1.计算机访问内存的方式是一个字节一个字节的访问的
2.一个引用(机器地址)需要8个字节来表示
    例如: Date date = new Date(), 则变量date需要占用8个字节位表示
3.创建一个对象,自身的开销是16字节,用来保存对象头部信息
4.一般内存的使用,如果不足8个字节,都会自动填充为8字节
    例: public class A{
            public int a=1;
        }
        通过new A()创建对象的内存:
            1.整型成员变量 a占用4个字节
            2.对象本身16个字节
            那么一共需要20个字节,但是会自动填充为8的倍数,24字节
5.一个原始"数组"类型,一般需要24字节头信息(16字节自身开销,4字节长度,4字节填充字节)
  再加上保存值所需的内存

排序

冒泡排序

在这里插入图片描述

代码实现
public class BubbleSort {
    /*
      对数组a中的元素进行排序
    * */
    public static void sort(Comparable[]a){
        //初始元素最大索引为"数组长度-1",第一个元素索引为"0"
        //它之前已经没有元素了,不需要再比较了,所以停止条件需要大于第一个元素的索引值0
        for (int i = a.length-1; i >0; i--) {
            for(int j=0;j<i;j++){
                //比较索引j和索引j+1的值,并进行交互位置
                if(greater(a[j],a[j+1])){
                    exchange(a,j,j+1);
                }
            }
        }
    }
    /*
      比较v元素是否大于w元素
    * */
    private static boolean greater(Comparable v,Comparable w){
        //v.compareTo(w)返回true时,说v>w
        return v.compareTo(w)>0;
    }
    /*
      数组元素i和j交换位置
    * */
    private static void exchange(Comparable[] a, int i, int j){
        Comparable temp;
        temp = a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
/**冒泡排序测试
 * */
@Test
public void testBubbleSort(){
    //Integer包装类实现了Comparable<Integer>接口
    Integer[] arr = {4,5,6,3,2,1};
    BubbleSort.sort(arr);
    System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6]

}
时间复杂度分析
冒泡排序使用了双层for循环,其中内层循环体才真正完成排序
最坏情况下如果要排序的元素为{6,5,4,3,2,1}的逆序
那么比较次数为:
    (N-1)+(N-2)+(N-3)+...2+1=(N^2-N)/2
元素交换次数:
    (N^2-N)/2
总执行次数:
    N^2-N
时间复杂度:
    O(N^2)  

选择排序

1.每一次遍历的过程汇总,都假设第一个索引处0的元素是最小值,和其他元素进行比较
  找出最小元素所在的索引值,重点也是这句话:
      "最小元素的所在的索引值"
      假设minIndex=0,此时索引0处值为4,不断比较,一旦发现比4小的值,产生交换
      最后发现最小值是1,此时minIndex=7,产生交换,索引0和索引7的值交换
      此时索引0的值为1,索引7的值为4,外层控制循环++,假设minIndex=1
      ......
2.交换第一个索引处和最小值所在索引处的值   
3.每次找到一个最小值后,都只在剩余的元素中进行比较,并且剩余的每个元素都会被比较 

在这里插入图片描述

代码实现
public class SelectionSort {

    /*对数组a中的元素进行选择排序
    * */
    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-1;j++){
                //比较最小索引minIndex处的值和j索引处的值
                if(greater(a[minIndex],a[j])){
                    minIndex=j;//索引值交换
                }
            }
            //交换最小元素所在索引minIndex处的值和索引i处的值
            exchange(a,i,minIndex);
        }
    }

    /*比较v元素是否大于w元素
    * */
    public static boolean greater(Comparable v,Comparable w){
        return v.compareTo(w)>0;
    }

    /*数组元素i和j交换位置
    * */
    public static void exchange(Comparable[] a, int i, int j){
        Comparable temp;
        temp=a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
/**选择排序测试
 * */
@Test
public void testSelectSort(){
    Integer[] arr = {4,6,8,7,9,2,10,1};
    SelectionSort.sort(arr);
    System.out.println(Arrays.toString(arr));//[1, 2, 4, 6, 7, 8, 9, 10]
}
时间复杂度分析
选择排序使用了双层for循环,其中外层完成了数据交换,内层循环完成了数据比较
数据比较次数:
    (N-1)+(N-2)+(N-3)+...2+1 = (N^2-N)/2
数据交换次数:
    N-1
时间复杂度:
    (N^2-N)/2+(N-1) = (N^2+N-1)/2 = O(N^2)  

插入排序

1.把所有的元素分为两组,已经排序和未排序的
2.找到未排序的组中第一个元素,向已经排序的组中进行插入
3.倒序遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素
  那么就把待插入元素放在这个位置,其他元素向后移一位
  
与冒泡排序其实很类似,冒泡排序也是每次交换完必定会产生已排序的和未排序的
但不同的是,冒泡排序需要和每个元素都进行比较,而插入排序是倒序遍历的,我们
已知倒序比较的第一个元素,一定是已排序元素组中最大的元素,一旦待插入的元素
比最大的元素还大的话,就没必要继续比较下去了,直接跳出循环.
例如下图:
    第三趟排序,待插入的元素是10,10前面的元素都是已从小到大排序的,我们
    只需要把104比较一次即可,而不用和元素4前面的元素继续比较  
    
假设有这样一个数组为 list[]{1,2,3,4}; 
我们要对其进行升序排序(很显然 这里已经是符合要求的升序排列).
现在 假若我们用冒泡排序 大概流程会是这样:
    1.先将12进行比较 无须替换 然后23比较 无须替换 然后34比较 无须替换 完成第一轮冒泡 比较次数为32.23进行比较 无须替换 然后34进行比较 无须替换 完成第二轮冒泡 比较次数为23.34进行比较 无须替换 完成第三轮冒泡 比较次数为1次
    很显然 我们没有移动一次数字 但是却比较了6次
再来看看插入排序如何实现 其详细步骤为:
    1.先设定1为有序区间 将21比较 无须移动 比较一次
    2.此时12均为有序区间 将32进行比较 无须移动 直接跳出while循环 比较一次
    3.此时123均为有序区间 将43进行比较 无须移动 直接跳出while循环 比较一次
    很显然 我们也没有移动数字 但是只比较了3"针对部分有序的集合来说,插入排序要优于冒泡排序,而无序的情况下,它们的效率是一样的"

在这里插入图片描述

代码实现
public class InsertionSort {

    /*对数组a中的元素进行选择排序
     * */
    public static void sort(Comparable[] a){
        //因为第一个元素默认为已排序的,所以初始已排序处索引为1,最大为最大索引
        for(int i=1;i<a.length;i++){
            //待排序的元素j=i,由于是倒序遍历进行比较,所以j能插入最小的索引位置要大于0
            for(int j=i;j>0;j--){
                //如果索引j-1处的值大于索引j处的值,则交换
                if(greater(a[j-1],a[j])){
                    exchange(a,j-1,j);
                }else{//否则退出循环,如果不退出循环,程序将一直进行
                    break;
                }
            }
        }
    }

    /*比较v元素是否大于w元素
     * */
    public static boolean greater(Comparable v,Comparable w){
        return v.compareTo(w)>0;
    }

    /*数组元素i和j交换位置
     * */
    public static void exchange(Comparable[] a, int i, int j){
        Comparable temp;
        temp=a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
/**插入排序测试
 * */
@Test
public void testInsertion(){
    Integer[] arr = {4,3,2,10,12,1,5,6};
    InsertionSort.sort(arr);
    System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6, 10, 12]
}
时间复杂度分析
使用双层for循环,内层循环完成排序代码,最坏情况下:
比较次数:
    (N^2-N)/2
交换次数:
    (N^2-N)/2
总执行次数:
    N^2-N
时间复杂度:
    O(N^2) 

希尔排序

希尔排序是插入排序的一种,又称"缩小增量排序",是插入排序的更高效的版本
    与插入排序对比,如果存在这样的情况:已排序的分组元素为 {2,5,7,9,10}
    未排序的元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,
    依次和10,9,7,5,2进行交换位置,而希尔排序改进的就是,减少比较的次数
排序原理:
    1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
    2.对分好组的每一组数据完成插入排序
    3.减少增长量,最小为1,重复第二步操作
增长量h的确定规则:
    int h=1;
    while(h<数组的长度/2){
        h=2h+1
    }
    //循环结束后后可以确定h的最大值
    h=h/2;  

在这里插入图片描述

代码实现
public class ShellSort {

    /*对数组a中的元素进行希尔排序
    * */
    public static void sort(Comparable[] a){
        //1.根据数组a的长度来确定增长量h的初始值
        int h=1;
        while(h<a.length/2){
            h=h*2+1;
        }
        //2.希尔排序
        while(h>=1){//h最小为1
            //2.1找到待插入的元素,待插入的元素不能比增长量h小,因为那样没办法比较
            for(int i=h;i<=a.length-1;i++){
                //2.2把待插入的元素插入到有序数列中,j每次减去一个h,即j下一次需要插入的位置
                for(int j=i;j>=h;j-=h){
                    //比较待插入元素a[j]的值和前一个有序数列a[j-h]的值
                    if(greater(a[j-h],a[j])){
                        exchange(a,j-h,j);
                    }else{
                        //待插入的元素已经找到了合适的位置(即这个元素比前一个元素大),结束循环
                        break;
                    }
                }
            }
            //减少h的值
            h=h/2;
        }
    }

    /*比较v元素是否大于w元素
     * */
    public static boolean greater(Comparable v,Comparable w){
        return v.compareTo(w)>0;
    }

    /*数组元素i和j交换位置
     * */
    public static void exchange(Comparable[] a, int i, int j){
        Comparable temp;
        temp=a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
/**希尔排序测试
 * */
@Test
public void testShellSort(){
    Integer[] arr = {9,1,2,5,7,4,8,6,3,6};
    ShellSort.sort(arr);
    System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6, 6, 7, 8, 9]
}
时间复杂度分析
时间复杂度
O(n^(1.5)) < O(n^2)
这是一个大致的时间复杂度,证明过程超过本次学习范围
相对于普通的插入排序,性能更优

归并排序

归并排序是建立在归并操作上的一种有效排序算法,采用分治法的一个典型应用.
将已有序的自序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序,
最后合并成一个有序表.

排序原理:
    1.尽可能的把一组数据拆分成两个元素相等的子组,并继续拆分,直到每个子组的个数都为一
    2.将相邻的两个子组进行排序,合并成一个有序的大组(归并的前提条件就是需要归并的数组是有序的)
    3.不断重复步骤二,直到最终只有一个组为止

在这里插入图片描述

代码实现
public class MergeSort {
    //归并所需的辅助数组
    private static Comparable[] assist;

    /*对数组a中的元素进行排序
    * */
    public static void sort(Comparable[] a){
        //初始化辅助数组assist
        assist = new Comparable[a.length];
        //定义一个low变量和high变量,分别记录最小索引和最大索引
        int low = 0;
        int high = a.length-1;
        //调用sort重载方法,完成从low到high的元素排序
        sort(a,low,high);
    }

    /*对数组a中从low到high的元素进行排序
    * */
    private static void sort(Comparable[] a, int low, int high){
        //做安全性校验,当子序列中只有一个元素时结束递归
        if(high<=low){
            return;
        }else{
            //对low到high之间的数据分成两个组,定义中间索引mid
            int mid = (low+high)/2;
            //对两个分组分别递归调用进行单独排序
            sort(a,low,mid);
            sort(a,mid+1,high);
            //最后将两个组中数据进行归并
            merge(a,low,mid,high);
        }

    }

    /*对数组中,从low到mid为一组,从mid+1到high为一组,对两组数据进行归并
    * */
    public static void merge(Comparable[] a, int low, int mid, int high){
        //定义三个指针,p1,p2分别指向两个数组的初始索引,i则指向辅助数组的初始索引
        int i = low;
        int p1 = low;
        int p2 = mid+1;
        //遍历,移动p1,p2指针,比较索引处的值,找出小的那个,放到对应索引处
        while (p1<=mid && p2<=high){
            if(less(a[p1],a[p2])){
                assist[i++] = a[p1++];
            }else{
                assist[i++] = a[p2++];
            }
        }
        //还需要考虑两种情况,任意一个数组元素先复制完成,下面的循环会执行其中一个
        //如果上面循环退出条件是p1<=mid,则说明左组归并完毕,如果退出条件是p2<=high,则说明,右组归并完成
        //遍历,如果指针p1没有走完,而p2已经走完,没有元素了,则顺序移动p1,把元素放到辅助数组对应索引处
        while (p1<=mid){
            assist[i++] = a[p1++];
        }
        //遍历,如果指针p2没有走完,而p1已经走完,没有元素了,则顺序移动p2,把元素放到辅助数组对应索引处
        while (p2<=high){
            assist[i++] = a[p2++];
        }
        //把辅助数组assist中的元素拷贝到原数组中
        for(int index=low; index <= high; index++){
            a[index] = assist[index];
        }
    }
    /*比较v元素是否小于w元素
    * */
    private static boolean less(Comparable v,Comparable w){
        return v.compareTo(w)<0;
    }
}
时间复杂度分析
假设元素的个数是n,那么需要拆分log2(n),log2(n)层
自顶向下第K层有2^K个子数组,每个数组长度为2^(3-k),归并最多需要比较2^(3-k)次比较
每层需要比较2^K,K层则是K*2^K次比较次数
时间复杂度:
    O(nlog2n)
缺点:
    需要申请额外的数组空间,导致空间复杂度提示,是用空间换时间  

快速排序

快速排序是对冒泡排序的一种改进
基本思想:
    通过一趟排序将要排序的数据以一个分界值,分割成独立的两部分,其中分界值左边数据都比分界值右边数据小
    然后重复此方法,对两部分数据分别快速排序,递归进行,达到整个数据变成有序
    
排序原理:
    1.设定分界值,通过分界值将数组分为左右两部分
    2.将大于或等于分界值的数据放数组右边,小于的放左边  
    3.重复步骤二,做类似递归处理
    
核心方法在于如何进行分组交换
    //首先定义两个指针,分别指向待切分元素的最小索引处和最大索引下一个位置(指针其实就是索引的抽象表示)
    int left =low;
    int right = high+1;
    确定分界值key为数组第一个元素后,分组方法中做了三件事
    1.通过while循环控制右指针扫描,找到比key小的元素,即停下,跳出这个循环
    2.进行第二个while循环,找到比key大的元素,指针停下,跳出这个循环
    3.条件判断,左指针是否大于或者等于右指针的索引了,如果是,说明扫描完毕了
      如果不是,则交换左指针和右指针的值,并且继续重复上面步骤  
      最后结束整个循环后,把分界值和左右指针指向的位置进行交换  

在这里插入图片描述在这里插入图片描述在这里插入图片描述

代码实现
public class QuickSort {

    /*对数组内元素进行快速排序*/
    public static void sort(Comparable[] a){
        int low=0;
        int high=a.length-1;
        sort(a,low,high);
    }

    /*对数组a中从索引low到high之间的元素进行排序*/
    private static void sort(Comparable[] a, int low, int high){
        if(low>=high){
            return;
        }else{
            //进行分组,分为左子组和右子组
            int par = partition(a, low, high);//返回分界值变换后的索引
            //让左子组有序
            sort(a,low,par-1);
            //让右子组有序
            sort(a,par+1,high);
        }
    }
    /**核心部分*/
    /*对数组a中,从索引low到high之间的元素进行分组,并且返回分组界限对应的索引*/
    public static int partition(Comparable[]a, int low, int high){
        //确定分界值
        Comparable key = a[low];
        //定义两个指针,分别指向待切分元素的最小索引处和最大索引下一个位置
        int left =low;
        int right = high+1;
        //切分
        while (true){
            //先从右往左扫描,移动right指针,目标找到一个比分界值小的元素
            while (greater(a[--right],key)){//右指针扫描元素比分界值key大的话,说明右指针没有找到,需要继续移动
                if(right==low){
                    break;
                }
            }
            //再从左往右扫描,移动left指针,目标找到一个比分界值大的元素
            while (greater(key,a[++left])){//分界值key比左指针扫描元素大的话,说明左指针没有找到,需要继续移动
                if(left==high){
                    break;
                }
            }
            //判断left>=right,如果是,说明扫描完毕,结束循环,如果不是,则交换元素
            if(left>=right){
                break;
            }else{
                exchange(a,left,right);//left指针扫描比key大的元素,right指针扫描比key小的元素,然后交换
            }
        }
        //交换分界值
        exchange(a,low,right);//分界值即left或者right,因为它们指向同一个位置
        return right;
    }

    private static boolean greater(Comparable v, Comparable w){
        return v.compareTo(w)>0;
    }

    private static void exchange(Comparable[] a, int i, int j){
        Comparable temp;
        temp=a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
时间复杂度分析
快速排序和归并排序,都是分治思想中的排序算法
把一个大问题分成若干小问题,最后将小问题的解法组成大问题的解法
区别:
    1.快速排序不需要备用数组,直接在待排序序列中排序,而归并排序需要
    2.快速排序是在两个子数组都有序的时候归并,归并完后直接是有序的
      而归并排序将有序子数组归并后,还要进行排序,才能使整个数组有序
    3.归并排序,数组被等分,基准值就是数组中间值
      快速排序,切分数组的位置取决于数组的内容  
时间复杂度分析:
    最优情况:每一次切分选择的基准数字刚好和序列等分,切logn次,时间复杂度是O(logn)
    最坏情况:每一次切分选择的基准数组刚好是数组的最大值或最小值,需要切n次,时间复杂度是O(n^2)
    平均情况:时间复杂度O(nlogn)

排序的稳定性

稳定性的含义

数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,
如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的

在这里插入图片描述

稳定性的意义
如果只是简单的进行数字的排序,那么稳定性将毫无意义
除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义
那么我们需要在二次排序的基础上保持原有排序的意义
例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,
使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序
这样可以减少系统的开销,因为如果下一次再进行价格高低排序的时候,可能会有之前已经排序数据,减少再排序
-------------------
如下图中,就是一种稳定的排序

在这里插入图片描述

稳定性比较
冒泡排序:
    只有当arr[i]>arr[i+1]的时候,才会改变位置,相等的时候并不会改变,所以是稳定的
选择排序:
    例如数据{5(1), 8, 5(2), 3, 9}里面有五个数,其中5是重复的,第一遍选择最小元素为3,
    需要和第一个元素5(1)进行交换,5(1)到了5(2)的后面,破坏了稳定性,所以是不稳定的
插入排序:
    只有前一个元素a[j-1]大于待插入元素a[j]的时候,才会交换位置,等于的时候并不会交换
    插入排序是稳定的
希尔排序:
    希尔排序是对数据分组,每一组进行插入排序,如果进行多次插入,其稳定性可能会被打破
    是不稳定的
归并排序:
    只有当arr[i]<a[i+1]的时候,才会交换位置,元素相等也不会交换,所以是稳定的  
快速排序:
    快速排序需要一个基准值,例如数据{6,9,5(1),8,5(2)},此时,基准值为6,右指针移动扫描到值5(2),
    左指针开始扫描,找到值9,这两个值交换后{6,5(2),5(1),8,9},这样两个5就发生了交换,破坏稳定性  

总结

稳定排序: 冒泡,插入,归并
不稳定排序: 选择,希尔,快速
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值