数据结构与算法 入门 与 排序

数据结构与算法

1.概述

1.1什么是数据结构

  • 数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据
  • 数据结构 = 元素 + 元素的关系

1.2数据结构的分类(元素的关系)

数据结构可以分为逻辑结构和物理结构两大类

1.2.1逻辑结构分类

使用数据元素之间的逻辑关系经行分类,主要可一分为线性结构、与非线性结构。

  • 线性结构
    • 数据结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
    • 线性结构右两种不同的存储结构,即顺序存储结构何链式存储结构,顺序存储的线性表称为顺序表,顺序表中存储元素是连续的(主要是地址连续)
    • 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息

    线性结构常见的有:数组、队列、列表何栈。

  • 线性结构特征:

    • 集合中必存在唯一的一个“第一个元素”;
    • 集合中必存在唯一的一个”最后的元素“;
    • 除最后元素之外,其它数据元素均有唯一的”后继“;
    • 除第一元素之外,其它数据元素均有唯一的”前驱“。
  • 非线性结构

    非线性结构中各个数据元素不再保持在一个线性序列中,每个数据元素可能与零个或者多个其他数据元素发生联系。根据关系的不同,可分为层次结构和群结构。

    非线性结构包括:二维数组,多维数组,广义表,树结构,图结构

1.2.2再具体经行分类

主要有 线性结构 、集合、树形、图状。

  • 集合:集合结构中数据除了属于同一个集合外,他们之间没有任何的关系。
  • 线性结构:数据元素中存在一对一的关系
  • 树形结构:数据元素存在一对多的关系
  • 图形结构:数据元素存在多对多的关系。
1.2.3物理结构分类

​ 逻辑节后再计算机中真正的表达方式(又成为映像)称为物理结构。

数据元素的存储结构形式有两种:**顺序存储**和**链式存储**
  • 顺序结构

    顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的,像生活中的排队。我们可以通过索引来找到数据元素。

    如果就是这么简单和有规律,一切就好办了 。可实际上 ,总会有人插队,也会有人要上厕所、有人会放弃排队。所以这个队伍当中会添加新成员,也有可能会去掉老元素,整个结构时刻都处于变化中。显然,面对这样时常要变化的结构,顺序存储是不科学的 。 那怎么办呢? 此时就出现了我们的链式存储结构。

  • 链式存储结构

    链式存储结构把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的 。 数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。

1.3什么是算法

算法就是:解决问题的方法和步骤

衡量算法有如下标准:

  • 时间复杂度:程序要执行的次数,并非执行时间

  • 空间复杂度:算法执行过程中大概要占用的最大内存

  • 难易程度(可读性)

  • 健壮性

1.3.1生活中的算法

再生活中你遇到某个问题有多个解决方案。

  • 比如从a到b你可以选择飞机、汽车、步行等。 不同的方案带来的成本也是不同的,坐飞机花费金钱最多,单所用时间最短。步行花费金钱最少,使用时间最长。

在程序中,我们也可以使用不同的算法解决相同的问题,而不同的算法成本也是不同的,而优秀的算法追求两个目标:

  1. 花最少的时间完成需求
  2. 占用最少的空间完成需求

2.算法分析

算法的目的就是为了在花费少的时间,占用少的内存完成需求。那么花费少的时间我们称为时间复杂度分析,占用少的内存叫做空间复杂度分析。

通常,对于一个给定的算法,我们要做 两项分析。第一是从数学上证明算法的正确性,这一步主要用到形式化证明的方法及相关推理模式,如循环不变式、数学归纳法等。而在证明算法是正确的基础上,第二部就是分析算法的时间复杂度。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。因此,作为程序员,掌握基本的算法时间复杂度分析方法是很有必要的。

2.1时间复杂度分析

算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。而度量一个程序的执行时间通常有两种方法。

2.1.1事后分析法

在给定编译好的算法,通过测试程序运行来经行时间的比较

这种方法可行,但不是一个好的方法。该方法有两个缺陷:

  • 一是要想对设计的算法的运行性能进行评测,必须先依据算法编制相应的程序并实际运行;
  • 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优势。

通过一个简单的例子来说明

   public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int sum =0;
        int n = 100;
        for (int i = 0; i <= n; i++) {
            sum+=i;
        }

        System.out.println("sum:"+sum);
        long end = System.currentTimeMillis();
        System.out.println("所花费的时间"+(end-start));
    }

这是一个最简单的程序,当我们有大量的程序代码,我们编写测试的程序也是需要花费大量的时间。如果这个算法很垃圾,那么之前的做的测试等事就白做了

2.1.2事前分析法

因事后统计方法更多的依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。因此人们常常采用事前分析估算的方法。

在编写程序前,依据统计方法对算法进行估算。一个用高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:

  • 算法采用的策略、方法;
  • 编译产生的代码质量;
  • 问题的输入规模;
  • 机器执行指令的速度。

如果算法固定,那么算法的执行时间就只和输入规模有关系了。

一个算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作的重复执行的次数作为算法的时间量度。

通过一个简单的例子来说明

  • 第一种
用户输入量n=1时,则计算需要一次
用户输入量n=100时,则计算需要一百次    
public static void main(String[] args) {
    int sum =0;   //执行1次
    int n = 100;  //执行1次
    for (int i = 0; i <= n; i++) {  //执行n+1次
        sum+=i;  //执行n次
    }
    System.out.println("sum:"+sum);

}
  • 第二种
用户输入量n=1时,则计算需要一次
用户输入量n=100时,则计算需要一次    
public static void main(String[] args) {
    int sum =0;   //执行1次
    int n = 100;  //执行1次
	sum = (n+1)*n/2;
    System.out.println("sum:"+sum);

}

如果我们把第一种算法的循环体看作时一个整体,忽略结束判断,那么这个两个算法运行时间的差距就是n和1的差距。

在我们分析一个算法的运行时间,最重要的就是把核心操作次数和输入规模关联起来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lz63E9C9-1597399150633)(C:/Users/84411/Desktop/学习笔记/学习笔记-数据算法与结构/imgs/01.png)]

2.1.3函数渐进增长

概念:给定两个函数f(n)和g(n) ,如果存在一个整数N,使其对于所有的n>N,f(n)总是比g(n) 大,那么说明f(n)的增长渐进块于g(n).

例:上面的f(n)=n^2>f(n)=n.

注意点:

  1. 随着数据规模的增大,算法的常数操作可以忽略不计。
  2. 随着数据规模的增大,与最高次项相乘的常数可以忽略。
  3. 最高此项的指数大的,随着n的增长会变得特别块
2.1.4时间复杂度
  • 时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
  • 时间复杂度 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
  • 执行次数=执行时间

2.2空间复杂度分析

  • 是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

2.排序

在我们的程序中,排序是非常常见的一种。我一些数据 按照不同的规则来进行排序,如商品价格、订单等

2.1 Comparable接口

Comparable 是排序接口。

若一个类实现了Comparable接口,就意味着“该类支持排序”。 即然实现Comparable接口的类支持排序,假设现在存在“实现Comparable接口的类的对象的List列表(或数组)”,则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。

此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。

我们可以点进Comparable 接口 ,可以看到只有一个方法。

package java.lang;
import java.util.*;

public interface Comparable<T> {

    public int compareTo(T o);
}

例子:

根据Student 对象的年龄进行排序

import lombok.Data;

@Data
public class Student implements Comparable<Student> {

 private String name;
 private int age;

 public int compareTo(Student o) {
     return this.age-o.age;
 }
}
class StudentTest {

 public static void main(String[] args) {
     Student student1 = new Student();
     student1.setName("张三");
     student1.setAge(18);
     Student student2 = new Student();
     student2.setName("王五");
     student2.setAge(20);
     Comparable max = getMax(student1, student2);
     System.out.println(max);
 }

 public static Comparable getMax(Comparable o1, Comparable o2){

     int i = o1.compareTo(o2);
     /**
        *  如果i>0  则o1>02
        *  如果i<0  则o1<02
        *  如果i=0  则o1=02
        */

       if (i>0){
           return o1;
       }else {
           return o2;
       }
   }
}

2.2 Comparator接口

Comparator 是比较器接口。

我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。

也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。

我们可以点进Comparator接口 :

package java.util;

public interface Comparator<T> {

    int compare(T o1, T o2);

    boolean equals(Object obj);
}
  • 若一个类要实现Comparator接口:它一定要实现compareTo(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数。

    为什么可以不实现 equals(Object obj) 函数呢? 因为任何类,默认都是已经实现了equals(Object obj)的。 Java中的一切类都是继承于java.lang.Object,在Object.java中实现了equals(Object obj)函数;所以,其它所有的类也相当于都实现了该函数。

  • int compare(T o1, T o2) 是“比较o1和o2的大小”。返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”。

区别:

​ Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。
​ 而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。

2.3简单排序

2.3.1冒泡排序
  • ​ 排序原理:
    • 比较相邻的元素。如果前一个比后一个元素大,则这两个交换位置。
    • 对每一对相邻的元素做同样的工作,从开始第一对到元素。最终的位置就式最大值。
冒泡次数冒泡结果
初始状态6 4 5 3 2 1
1次冒泡4 5 3 2 1 6
2次冒泡4 3 2 1 5 6
3次冒泡3 2 1 4 5 6
4次冒泡2 1 3 4 5 6
5次冒泡1 2 3 4 5 6
6次冒泡1 2 3 4 5 6

实现:

  1. 实现简单的数组排序

    import java.util.Arrays;
    
    public class BubbleA {
        public static void main(String[] args) {
            int[] arr ={5,2,6,4};
    
            sort(arr);
            System.out.println(Arrays.toString(arr));
    
        }
    
        public static  void  sort(int[] arr){
            int temp;
    
            for (int i = 0; i < arr.length-1; i++) {
                for (int j = 0; j < arr.length-1; j++) {
                    if (arr[i+1]<arr[j]){
                        temp = arr[j];
                        arr[j]= arr[i+1];
                        arr[i+1] = temp;
                    }
                }
            }
        }
    }
    
    
  2. Api实现

    1. 比较元素大小方法:greater()
    2. 位置交换:exch()
    3. 排序 :sort()
    import java.util.Comparator;
    
    public class BubbleB {
        /**
         * 对数据进行排序
         */
        public static void sort(Comparable[] a){
            for (int i = a.length - 1; i > 0 ; i--) {
                for (int j = 0; j < i; j++) {
    
                    if (greater(a[j],a[j+1])) exch(a,j,j+1);
    
                }
            }
        }
    
        public static boolean greater(Comparable m ,Comparable n ){
            return m.compareTo(n)>0;
        }
    
        public static void exch(Comparable[] a, int i, int j){
            Comparable temp;
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    
    }
    
    
        public static void main(String[] args) {
            Integer[] arr = { 1 ,5, 10,6,5};
            BubbleB.sort(arr);
    
            System.out.println(Arrays.toString(arr));
        }
    

2.3.2冒泡排序的算法分析

冒泡排序使用了双层for循环,其中内层循环体中的代码是真正完成排序的,所以我们分析冒泡排序的时间复杂度,主要分析一下内层循环体的执行次数即可。

最坏情况就是倒序:

​ 例:

​ 元素比较次数:(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;

​ 总执行次数: (n2/2-n/2)+(n2/2-n/2) = n^2-n

按照大O推到法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为O(n^2);

2.3.3选择排序

是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

实现:

package 数据结构与算法.排序.选择排序;

import java.util.Arrays;

public class SelectionA {


 public static void sort(Comparable[] a){

     int n =  a.length - 2;

     for (int i = 0; i <= n; i++) {
         //定义一个变量,记录最小元素的位置
         int min = i ;

         for (int j = i+1; j < a.length; j++)  if (greater(a[min],a[j])) min = j;

         //交换最小元素索引 min 处的值和j索引i处的值
         exch(a,i,min);

         }


 }


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

 public static void exch(Comparable[] a, int i, int j) {
     Comparable t = a[i];
     a[i] = a[j];
     a[j] = t;
 }


 public static void main(String[] args) {
     Integer[] arr = { 1 ,5, 10,6,5};

     SelectionA.sort(arr);
     System.out.println(Arrays.toString(arr));

 }
}

平均时间复杂度О(n²)
最坏时间复杂度О(n²)
最优时间复杂度О(n²)
空间复杂度总共О(n),需要辅助空间O(1)
2.3.4插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

Insertion Sort 和打扑克牌时,从牌桌上逐一拿起扑克牌,在手上排序的过程相同。

举例:

​ Input: {5 2 4 6 1 3}。

​ 首先拿起第一张牌, 手上有 {5}。

​ 拿起第二张牌 2, 把 2 insert 到手上的牌 {5}, 得到 {2 5}。

​ 拿起第三张牌 4, 把 4 insert 到手上的牌 {2 5}, 得到 {2 4 5}。

​ 以此类推。

实现:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

可以采用二分查找法来减少“比较操作”的数目,而由于“交换操作”的数目不变,算法的时间复杂度依旧为О(n²) 。该算法可以认为是插入排序的一个变种,称为二分查找插入排序

实现:

public class Insertion {


 public static void sort(Comparable[] a){
     for (int i =  1; i < a.length ; i++) {

         for (int j = i; j >= 0; j--) {
             //比较索引j处的值和索引j-1处的值,如果索引j-1处的值大 则交换数据,如果不大,那么找到合适的位置 ,退出循环。
             if (greater(a[j-1],a[j])) exch(a,j-1,j);
             else break;
             
         }
     }
 }

 public static boolean greater(Comparable m ,Comparable n ){
     return m.compareTo(n)>0;
 }

 public static void exch(Comparable[] a, int i, int j){
     Comparable temp;
     temp = a[i];
     a[i] = a[j];
     a[j] = temp;
 }
}

平均时间复杂度О(n²)
最坏时间复杂度О(n²)
最优时间复杂度О(n²)
空间复杂度总共О(n),需要辅助空间O(1)

2.4高级排序

2.4.1希尔排序

希尔排序(Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

实现原理:

  1. 选定一个增长量h ,按照h的长度对数据经行分组。
  2. 对分组的每一组元素经行插入排序
  3. 减少增长量,最少减为1,重复第二部操作。

列:数组{20,10,5,6,4,8} h = 3

次数数据增长量
020,10,5,6,4,8h=3
16 , 4 , 5, 20, 10, 8h=3
25, 4, 6,8,10,20h=2
34,5,6,8,10,20h=1
  • 增长量规则
public class ShellH {
    /**
     * 增长量的规则
     */

    public static void main(String[] args) {
        int[] arr={20,10,5,6,4,8};
        int h = 1;
        while (h<arr.length/2){   // h < 数组长度/2 
            h=2*h+1;
        }
        System.out.println(h);
    }
    /**
     *h 减小的规则	
     * 		h=h/2
     */

}

实现:

public static void shellSort(int[] arr) {
     int length = arr.length;
     int temp;
     for (int step = length / 2; step >= 1; step /= 2) {
         for (int i = step; i < length; i++) {
             temp = arr[i];
             int j = i - step;
             while (j >= 0 && arr[j] > temp) {
                 arr[j + step] = arr[j];
                 j -= step;
             }
             arr[j + step] = temp;
         }
     }
 }

API设计:

import java.util.Arrays;

public class Shell {

 /**
     * 对数组经行排序
     * @param a
     */
    public static void sort(Comparable[] a){
        int h =1;
        //1.根据数组长度确定增长量
        while (h<a.length/2) h=(2*h)+1;

        //希尔排序
        while (h>=1){
            //找到待插入的元素
            for (int i = h; i < a.length; i++) {
                //把待插入的元素插入到有序数列中
                for (int j = i; j >= h; j-=h) {
                    //比较a[j] a[j-h]
                    if (greater(a[j-h],a[j])){
                        //交换
                        exch(a,j,j-h);
                    }else {
                        break;
                    }
                }
            }
            //减小h的值
            h=h/2;

        }

    }

    public static boolean greater(Comparable m ,Comparable n ){
        return m.compareTo(n)>0;
    }

    public static void exch(Comparable[] a, int i, int j){
        Comparable temp;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    public static void main(String[] args) {
        Integer[] arr={20,10,5,6,4,8};

        Shell.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为普通插入排序,这就保证了数据一定会被排序。

Donald Shell最初建议步长选择为[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0ybj0oD-1597399150635)(https://wikimedia.org/api/rest_v1/media/math/render/svg/1216d48de276dc45542cb80b1e49037131ec9624)]并且对步长取半直到步长达到1。虽然这样取可以比[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p0tOxefz-1597399150637)(https://wikimedia.org/api/rest_v1/media/math/render/svg/4441d9689c0e6b2c47994e2f587ac5378faeefba)]类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。

步长序列最坏情况下复杂度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N2yznafl-1597399150639)(https://wikimedia.org/api/rest_v1/media/math/render/svg/6c8f4d868de17325e948844f4e4fc58fc0b0393a)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6DjdUpg-1597399150640)(https://wikimedia.org/api/rest_v1/media/math/render/svg/d6ae2ed4058fb748a183d9ada8aea50a00d0c89f)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bSLjrSVa-1597399150642)(https://wikimedia.org/api/rest_v1/media/math/render/svg/52376d02d45e1ac7c4cd8d8208c2f4ae104e3098)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiswvY8P-1597399150643)(https://wikimedia.org/api/rest_v1/media/math/render/svg/76af5b5bc7d056faef7eef0c6efeea3a16adc156)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8lLIMsyR-1597399150644)(https://wikimedia.org/api/rest_v1/media/math/render/svg/d6ae2ed4058fb748a183d9ada8aea50a00d0c89f)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5EEPZDU-1597399150644)(https://wikimedia.org/api/rest_v1/media/math/render/svg/0d30640651e80d1f38232384fdf407b253670801)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8IbzlKe-1597399150645)(https://wikimedia.org/api/rest_v1/media/math/render/svg/f2f4f16513454e80cf1f92f279d81914a8ee1e45)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSHFug8H-1597399150645)(https://wikimedia.org/api/rest_v1/media/math/render/svg/d6ae2ed4058fb748a183d9ada8aea50a00d0c89f)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pAi1Xlm-1597399150646)(https://wikimedia.org/api/rest_v1/media/math/render/svg/de727f8c182ebfc7dd8fe3ac86b2b15ec5dd6d5d)]
2.4.2归并排序
  • 递归: 自己调用自己

  • 归并排序

    归并排序是创建在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

  • 采用分治法:

    • 分割:递归地把当前序列平均分割成两半。
    • 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。
  • 排序原理

    1. 尽可能的把一组数据拆分成两个相等的子组,并对每一个子组再经行拆分,直到子组元素个数为1;
    2. 将相邻的两个子组经行合并成一个有序的大组
    3. 不断的重复2直到只有一个组为止;

实现:

import java.util.Arrays;

/**
 *  归并排序
 */
public class Merge {

    //辅助数组
    private static Comparable[] assist;


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

    }

    /**
     * 对数组a中 lo 到 hi 进行排序
     * @param a
     * @param lo
     * @param hi
     */
    public static void sort(Comparable[] a,int lo , int hi){
        if (hi<=lo) return;

        //分组 分两组
        int mid = lo+(hi-lo)/2;

        //对每一组数据经行排序
        sort(a,lo,mid);
        sort(a,mid+1,hi);

        //归并
        merge(a,lo,mid,hi);
    }


    /**
     * 对数据进行归并
     * @param a
     * @param lo
     * @param mid
     * @param hi
     */
    public static  void merge(Comparable[] a, int lo ,int mid, int hi){
        //定义三个指针
        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处的指针没有走完,那么顺序移动到p2指针,把对应的元素放到辅助数组的对应索引处
        while (p2<=hi) assist[i++] = a[p2++];

        //copy辅助数组

        for (int index = 0; index <= hi ; index++) {
            a[index] = assist[index];
        }

    }


    public static boolean less(Comparable v ,Comparable w ){
        return v.compareTo(w)<0;
    }

    public static void exch(Comparable[] a, int i, int j){
        Comparable temp;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }

    
}

2.4.3快速排序

​ 快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。

总的说来,要直接默写出快速排序还是有一定难度的,因为本人就自己的理解对快速排序作了下白话解释,希望对大家理解有帮助,达到快速排序,快速搞定。

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

该方法的基本思想是:

  • 1.先从数列中取出一个数作为基准数。
  • 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
  • 3.再对左右区间重复第二步,直到各区间只有一个数。

实现:

import java.util.Comparator;

public class Quick {

 public static void sort(Comparable[] a){
     int lo = 0 ;
     int hi = a.length-1;
     sort(a,lo,hi);

 }


 public static void sort(Comparable[] a,int lo , int hi){
     if (hi<=lo) return;

     int partition = partition(a, lo, hi);

     sort(a,lo,partition-1);

     sort(a,partition+1,hi);

 }



 //对数组a中,从索引hi之间的元素进行排序 并返回分组对应的索引
 public static  int partition(Comparable[] a, int lo , int hi){

     Comparable  key = a[lo];

     int left = lo;
     int right = hi+1;

     while (true){
         while (less(key,a[--right])){
             if (right==lo) break;
         }

     while (less(a[++left],key)){
         if (left==hi) break;
     }

     if (left>=right) break;
     else exch(a,left,right);

     }
     exch(a,lo,right);
     return right;
 }


 public static boolean less(Comparable v ,Comparable w ){
     return v.compareTo(w)<0;
 }

 public static void exch(Comparable[] a, int i, int j){
     Comparable temp;
     temp = a[i];
     a[i] = a[j];
     a[j] = temp;
 }



}

i);

 sort(a,lo,partition-1);

 sort(a,partition+1,hi);

}

//对数组a中,从索引hi之间的元素进行排序 并返回分组对应的索引
public static int partition(Comparable[] a, int lo , int hi){

 Comparable  key = a[lo];

 int left = lo;
 int right = hi+1;

 while (true){
     while (less(key,a[--right])){
         if (right==lo) break;
     }

 while (less(a[++left],key)){
     if (left==hi) break;
 }

 if (left>=right) break;
 else exch(a,left,right);

 }
 exch(a,lo,right);
 return right;

}

public static boolean less(Comparable v ,Comparable w ){
return v.compareTo(w)<0;
}

public static void exch(Comparable[] a, int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值