算法小记(一)

时间复杂度

这里面包含一个时间频度的概念,何为时间频度呢?就是一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。其中n表示问题的规模,一般情况下,T(n)就是算法中重复执行次数关于问题规模n的函数。若此时有另一个函数f(n),使得:
lim ⁡ n → + ∞ T ( n ) f ( n ) = C \lim_{ n \to + \infty}\frac{T(n)}{f(n)}=C n+limf(n)T(n)=C
则可以认为T(n)和f(n)为同数量级的函数,记做:
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
则称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
T (n) = Ο(f (n)) 表示存在一个常数C,使得在当n趋于正无穷时总有 T (n) ≤ C * f(n)
简单来说,就是T(n)在n趋于正无穷时最大也就跟f(n)差不多大。也就是说当n趋于正无穷时T (n)的上界是C * f(n)。其虽然对f(n)没有规定,但是一般都是取尽可能简单的函数。
例如,
O ( 2 n 2 + n + 1 ) = O ( 3 n 2 + n + 3 ) = O ( 7 n 2 + n ) = O ( n 2 ) O(2n^2+n +1) = O (3n^2+n+3) = O (7n^2 + n) = O ( n^2 ) O(2n2+n+1)=O(3n2+n+3)=O(7n2+n)=O(n2)
一般都只用
O ( n 2 ) O(n^2) O(n2)
表示就可以了。注意到O符号里隐藏着一个常数C,所以f(n)里一般不加系数。如果把T(n)当做一棵树,那么O(f(n))所表达的就是树干,只关心其中的主干,其他的细枝末节全都抛弃不管。

在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1),另外,在时间频度不相同时,时间复杂度有可能相同,如 T ( n ) = n 2 + 3 n + 4 T(n)=n^2+3n+4 T(n)=n2+3n+4 T ( n ) = 4 n 2 + 2 n + 1 T(n)=4n^2+2n+1 T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为 O ( n 2 ) O(n^2) O(n2)

按数量级递增排列,常见的时间复杂度有:常数阶 O ( 1 ) O(1) O(1),对数阶 O ( l o g 2 n ) O(log_2n) O(log2n),线性阶 O ( n ) O(n) O(n), 线性对数阶 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),平方阶 O ( n 2 ) O(n^2) O(n2),立方阶 O ( n 3 ) O(n^3) O(n3),…, k次方阶 O ( n k ) O(n^k) O(nk),指数阶 O ( 2 n ) O(2^n) O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

常见的算法时间复杂度由小到大依次为: O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < … < O ( 2 n ) < O ( n ! ) Ο(1)<Ο(log_2^n)<Ο(n)<Ο(nlog_2^n)<Ο(n^2)<Ο(n^3)<…<Ο(2^n)<Ο(n!) O(1)O(log2n)O(n)O(nlog2n)O(n2)O(n3)O(2n)O(n!)

求解时间复杂度步骤:

⑴ 找出算法中的基本语句;
 算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
 
⑵ 计算基本语句的执行次数的数量级;
  只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率

⑶ 用大Ο记号表示算法的时间性能。
  将基本语句执行次数的数量级放入大Ο记号中。

如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:

for (i=1; i<=n; i++)  
       x++;  
for (i=1; i<=n; i++)  
     for (j=1; j<=n; j++)  
          x++;  

则时间复杂度为(只关注增长率): O ( n ) + O ( n 2 ) → O ( n + n 2 ) → O ( n + n 2 ) → O ( n 2 ) O(n)+O(n^2) \to O(n+n^2) \to O(n+n^2) \to O(n^2) O(n)+O(n2)O(n+n2)O(n+n2)O(n2)

O ( 1 ) Ο(1) O(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是 O ( 1 ) Ο(1) O(1)。其中 O ( l o g 2 n ) 、 O ( n ) 、 O ( n l o g 2 n ) 、 O ( n 2 ) Ο(log_2^n)、Ο(n)、 Ο(nlog_2^n)、Ο(n^2) O(log2n)O(n)O(nlog2n)O(n2) O ( n 3 ) Ο(n^3) O(n3)称为多项式时间,而 O ( 2 n ) Ο(2^n) O(2n) O ( n ! ) Ο(n!) O(n!)称为指数时间。计算机科学家普遍认为前者(即多项式时间复杂度的算法)是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题。

在计算算法时间复杂度时有以下几个简单的程序分析法则:

(1).对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间

(2).对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下"求和法则"

求和法则:是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T_1(n)=O(f(n)) T1(n)=O(f(n)) T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ( n ) + T 2 ( n ) = O ( m a x ( f ( n ) , g ( n ) ) ) T_1(n)+T_2(n)=O(max(f(n), g(n))) T1(n)+T2(n)=O(max(f(n),g(n)))

特别地,若 T 1 ( m ) = O ( f ( m ) ) , T 2 ( n ) = O ( g ( n ) ) T_1(m)=O(f(m)), T_2(n)=O(g(n)) T1(m)=O(f(m)),T2(n)=O(g(n)),则 T 1 ( m ) + T 2 ( n ) = O ( f ( m ) + g ( n ) ) T_1(m)+T_2(n)=O(f(m) + g(n)) T1(m)+T2(n)=O(f(m)+g(n))

(3).对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要O(1) 时间

(4).对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"

乘法法则: 是指若算法的2个部分时间复杂度分别为 T 1 ( n ) = O ( f ( n ) ) T_1(n)=O(f(n)) T1(n)=O(f(n)) T 2 ( n ) = O ( g ( n ) ) T_2(n)=O(g(n)) T2(n)=O(g(n)),则 T 1 ∗ T 2 = O ( f ( n ) ∗ g ( n ) ) T_1*T_2=O(f(n)*g(n)) T1T2=O(f(n)g(n))

(5).对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度

另外还有以下2个运算法则: 若 g ( n ) = O ( f ( n ) ) , 则 O ( f ( n ) ) + O ( g ( n ) ) = O ( f ( n ) ) ; 若g(n)=O(f(n)),则O(f(n))+ O(g(n))= O(f(n)); g(n)=O(f(n)),O(f(n))+O(g(n))=O(f(n))

O ( C f ( n ) ) = O ( f ( n ) ) , 其 中 C 是 一 个 正 常 数 O(Cf(n)) = O(f(n)),其中C是一个正常数 O(Cf(n))=O(f(n)),C

实例:

O(1)

    Temp=i; i=j; j=temp;                    

以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作T(n)=O(1)。注意:如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。

O(n2)

交换i和j的内容

sum=0;                 (一次)  
for(i=1;i<=n;i++)     (n+1次)  
   for(j=1;j<=n;j++) (n2次)  
    sum++;            (n2次)  

解:因为 Θ ( 2 n 2 + n + 1 ) = n 2 Θ(2n^2+n+1)=n^2 Θ(2n2+n+1)=n2(Θ即:去低阶项,去掉常数项,去掉高阶项的常参得到),所以 T ( n ) = = O ( n 2 ) T(n)= =O(n^2) T(n)==O(n2)

for (i=1;i<n;i++)  
 {   
     y=y+1;         ①     
     for (j=0;j<=(2*n);j++)      
        x++;         ②        
 }            

解: 语句1的频度是n-1
语句2的频度是 ( n − 1 ) ∗ ( 2 n + 1 ) = 2 n 2 − n − 1 (n-1)*(2n+1)=2n^2-n-1 (n1)(2n+1)=2n2n1
f ( n ) = 2 n 2 − n − 1 + ( n − 1 ) = 2 n 2 − 2 f(n)=2n^2-n-1+(n-1)=2n^2-2 f(n)=2n2n1+(n1)=2n22
Θ ( 2 n 2 − 2 ) = n 2 Θ(2n^2-2)=n^2 Θ(2n22)=n2
该程序的时间复杂度 T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2).

一般情况下,对不进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分,当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。

O(n)

a=0;  
  b=1;                      ①  
  for (i=1;i<=n;i++) ②  
  {    
     s=a+b;    ③  
     b=a;     ④    
     a=s;     ⑤  
  }  

解: 语句1的频度:2,
语句2的频度: n,
语句3的频度: n-1,
语句4的频度:n-1,
语句5的频度:n-1,
T ( n ) = 2 + n + 3 ( n − 1 ) = 4 n − 1 = O ( n ) T(n)=2+n+3(n-1)=4n-1=O(n) T(n)=2+n+3(n1)=4n1=O(n).

O ( l o g 2 n ) O(log_2^n) O(log2n)

i=1;     ①  
while (i<=n)  
  i=i*2; ②  

解: 语句1的频度是1,
设语句2的频度是f(n), 则: 2 f ( n ) ≤ n ; f ( n ) ≤ l o g 2 n 2^{f(n)}\leq n;f(n)\leq log_2^n 2f(n)n;f(n)log2n
取最大值 f ( n ) = l o g 2 n f(n)=log_2^n f(n)=log2n,
T ( n ) = O ( l o g 2 n ) T(n)=O(log_2^n ) T(n)=O(log2n)

O ( n 3 ) O(n^3) O(n3)

for(i=0;i<n;i++)  
   {    
      for(j=0;j<i;j++)    
      {  
         for(k=0;k<j;k++)  
            x=x+2;    
      }  
   }  

解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,…,m-1 , 所以这里最内循环共进行了 0 + 1 + . . . + m − 1 = ( m − 1 ) m / 2 0+1+...+m-1=(m-1)m/2 0+1+...+m1=(m1)m/2次所以,i从0取到n, 则循环共进行了:
0 + ( 1 − 1 ) ∗ 1 / 2 + . . . + ( n − 1 ) n / 2 = n ( n + 1 ) ( n − 1 ) / 6 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6 0+(11)1/2+...+(n1)n/2=n(n+1)(n1)/6所以时间复杂度为 O ( n 3 ) . O(n^3). O(n3).

算法的空间复杂度

类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。渐近空间复杂度也常常简称为空间复杂度。
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法。

Start

首先介绍一个网站
https://www.lintcode.com
LintCode这个网站有各种算法的实战。

算法的大致分类如图:
在这里插入图片描述

一个简单的算法:

1)给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

2,2,1

2)进阶版:

1,2,2,1,3,4

不管有多么难我们都应该:
icon
思路:
1)我们发现只要是对所有元素做异或就可以很轻易的得到其中那个唯一的一个数。因为异或的解释就是相同为0相异为1
yihuo
2)根据前面找一个不同数的思路算法,在这里把所有元素都异或,那么得到的结果就是那两个只出现一次的元素异或的结果。因为这两个只出现一次的元素一定是不相同的,所以这两个元素的二进制形式肯定至少有某一位是不同的,即一个为 0 ,另一个为 1 ,现在需要找到这一位。
根据异或的性质任何一个数字异或它自己都等于 0,那么前面异或结果的这个数字的二进制形式中任意一个为 1 的位都是我们要找的那一位。
以这一位是 1 还是 0 为标准,将数组的 n 个元素分成两部分。

  • 将这一位为 0 的所有元素做异或,得出的数就是只出现一次的数中的一个
  • 将这一位为 1 的所有元素做异或,得出的数就是只出现一次的数中的另一个。
    这样就解出题目。忽略寻找不同位的过程,总共遍历数组两次,时间复杂度为O(n)
    122134
    icon2如果不太懂看我手写体:
    12234
    接下来难度升级
    icon

找出最长不重复字符串

给定一个字符串如下

"abcdefacdefgbhab"

找出其最长字符。
一般情况下:new两个变量,一个存最终结果,一个存临时最大长度字符串,依次遍历给定字符串,每次取一个,如果当前字符在存临时字符串里不存在,则append到后面,并且与最终结果做比较,把长的给最终结果。如下:

private String getLongStr(String s) {
        String result = "";
        StringBuffer temp = new StringBuffer();
        String tt = temp.toString();
        for (int i = 0; i < s.length(); i++) {
            String c = "";
            for (int j = 0; j < result.length(); j++) {
                c = tt.charAt(j) + "";
                if (c.equals(result.substring(i, i))) {
                    temp.delete(0, temp.length());
                }
            }
            temp.append(c);
            if (tt.length() > result.length()) {
                result = tt;
            }
        }
        return result;
    }

以上,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
换个思路,加入HashMap,只用利用键值对中键的唯一性来做。

private String getMaxString(String originString) {
        if (originString == null || originString.equals("")) {
            return "";
        }
        HashMap<Character, Integer> hashMap = new HashMap<>();
        String temp = "";
        int start = -1;

        for (int i = 0; i < originString.length(); i++) {
            if (hashMap.containsKey(originString.charAt(i)) && hashMap.get(originString.charAt(i)) > start) {
                start = hashMap.get(originString.charAt(i));
            }
            hashMap.put(originString.charAt(i), i);
            String s = originString.substring(start + 1, i + 1);

            if (s.length() > temp.length()) {
                temp = s;
            }
        }
        return temp;
    }

思路:
icona
与上面的思路一样,但是理由HashMap把每个字符作为键,把其对应的字符串位置作为值,这样遍历下来HashMap里面存的值就是最新的字符所在位置的值。temp就像一个是一个划窗,从第一个开始,窗体的起点就是start,终点是i,每次吸收一个,如果这个新的字符已经存在于键里面,则start就更新为当前的位置,窗体又从最小开始,直到大于最终的结果时,把当前的窗体内字符串给它,依次来选出最大字符串。
时间复杂度:这里原本循环里的时间复杂度为 O ( n ) O(n) O(n),即使而我们知道Hash的时间复杂度在不考虑Hash冲突的情况下,它的时间复杂度惊人的低,为 O ( 1 ) O(1) O(1)。故在Hash的加持下,我们算法的复杂度有原来的 O ( n 2 ) → O ( n ) O(n^2) \to O(n) O(n2)O(n)

接下来就是我们常常用到的一些似曾相识的排序算法了。
disp

八大排序算法:

直插排序

需求:把数据插入到已经排好序的数据中
思路:
直插排序

1.将前两个排序,构成一个有序序列。
2.将第三个数插入进去,构成一个新的有序序列。
3.重复第二步。

代码思路:
1.用一个变量记住要插入的那个数。
2.由已排好序的序列从后往前,依次比较,如果原始序列(已排好序的序列)数字大于备插数字,则原始序列往后推移一格,直到两数据相等或者原始序列(已排好序的序列)数字小于备插数字,流程结束。
3.最终把备插数字放到2中停止的位置。

public void insertSort(int[] a){
        int length=a.length;//数组长度
        int insertNum;//要插入的数
        for(int i=1;i<length;i++){
            insertNum=a[i];//要插入的数
            int j=i-1;//已经排序好的序列元素个数
            while(j>=0&&a[j]>insertNum){//序列从后到前循环,将大于insertNum的数向后移动一格
                a[j+1]=a[j];//元素移动一格
                j--;
            }
            a[j+1]=insertNum;//将需要插入的数放在要插入的位置。
        }
    }

验证:
直插排序

希尔排序

场景:对于直插排序数据量巨大时。它是对直插排序的升级。
思路:
1.每次以数据最大长度的二分之一为步长,选出相应的元素进行插入排序。
2.直到步长为0时,返回结果。
希尔排序

代码:
1.确定分组。
2.对组中元素做插入排序。
3.步长减半,重复1,2,直到步长为0。

public  void sheelSort(int[] a){
        int d  = a.length;
        while (d!=0) {
            d=d/2;
            for (int x = 0; x < d; x++) {//分的组数
                for (int i = x + d; i < a.length; i += d) {//组中的元素,从第二个数开始
                    int j = i - d;//j为有序序列最后一位的位数
                    int temp = a[i];//要插入的元素
                    for (; j >= 0 && temp < a[j]; j -= d) {//从后往前遍历。
                        a[j + d] = a[j];//向后移动d位
                    }
                    a[j + d] = temp;
                }
            }
        }
    }

优劣:
不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为 O ( n 3 2 ) O(n^\frac{3}{2}) O(n23),希尔排序时间复杂度的下界是 n ∗ l o g 2 n n*log2n nlog2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比 O ( n 2 ) O(n^2) O(n2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,今仍然是数学难题。

简单的选择排序/交换排序

场景:常用于去序列中最大最小的几个数使用的。如果每次比较后都交换那么就是交换排序,如果每次比较完一个循环再交换,那就是简单的选择排序。
思路:
1.遍历整个集合把最小的数放在最前面。
2.遍历剩下的,把最小的放在前面。
3.重复1,2直到只剩一个数。
因比较简单就贴代码。

public void selectSort(int[] a) {
        int length = a.length;
        for (int i = 0; i < length; i++) {//循环次数
            int key = a[i];
            int position=i;
            for (int j = i + 1; j < length; j++) {//选出最小的值和位置
                if (a[j] < key) {
                    key = a[j];
                    position = j;
                }
            }
            a[position]=a[i];//交换位置
            a[i]=key;
        }
    }

冒泡

和上面的类似,唯一的区别就是冒泡每次两两比较且换位置。

public void bubbleSort(int[] a){
        int length=a.length;
        int temp;
        for(int i=0;i<a.length;i++){
            for(int j=0;j<a.length-i-1;j++){
                if(a[j]>a[j+1]){
                    temp=a[j];
                    a[j]=a[j+1];
                    a[j+1]=temp;
                }
            }
        }
    }

看到冒泡,选择排序是不感觉很轻松,容易,那么下面来点有节奏的。
难

堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。
讲几个概念:
二叉树:在计算机科学中,二叉树是每个结点最多有两个子树的树结构。
完全二叉树:除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。
大根堆:父节点的值大于或等于子节点的值。(下面以大根堆为例!
小根堆:父节点的值小于或等于子节点的值。
完全二叉树性质:左边子节点的位置=其父节点位置的两倍+1 右边子节点的位置=其父节点位置的两倍+2
思路:
1.依据给定的数据,建立大根堆
2.交换根节点与尾节点,取出最大值
3.重复1,2
堆排序

代码思路:
由完全二叉树的性质可得到如下:
最 后 一 个 节 点 的 父 节 点 = 最 后 节 点 / 2 − 1 最后一个节点的父节点=最后节点/2 - 1 =/21
由上面的示意图我们可以发现堆排序就是建立大根堆或者小根堆,然后跟和最后节点互换位置,断开链接,继续建堆,互换断开链接…
则依据上面的性质可拟定代码思路:

  1. 找最后子节点的父节点,即数组末尾下标,获取父节点
  2. 排序整个待追究节点,即:根,左孩子,有孩子排序
  3. 父节点上移,排序整个节点
  4. 重复2,3 直到下标为0,即呈现出大根堆的状态
  5. 根与最后子节点互换,即a[0]与a[end]互换
  6. 数组length-1
  7. 重复2~6
public  void heapSort(int[] a){
        int arrayLength=a.length;
        //循环建堆
        for(int i=0;i<arrayLength-1;i++){
            //建堆
            buildMaxHeap(a,arrayLength-1-i);
            //交换堆顶和最后一个元素
            swap(a,0,arrayLength-1-i);
            System.out.println(Arrays.toString(a));
        }
    }
    private  void swap(int[] data, int i, int j) {
        // TODO Auto-generated method stub
        int tmp=data[i];
        data[i]=data[j];
        data[j]=tmp;
    }
    //对data数组从0到lastIndex建大顶堆
    private void buildMaxHeap(int[] data, int lastIndex) {
        // TODO Auto-generated method stub
        //从lastIndex处节点(最后一个节点)的父节点开始
        for(int i=(lastIndex-1)/2;i>=0;i--){
            //k保存正在判断的节点
            int k=i;
            //如果当前k节点的子节点存在
            while(k*2+1<=lastIndex){
                //k节点的左子节点的索引
                int biggerIndex=2*k+1;
                //如果biggerIndex小于lastIndex,即biggerIndex+1代表的k节点的右子节点存在
                if(biggerIndex<lastIndex){
                    //若果右子节点的值较大
                    if(data[biggerIndex]<data[biggerIndex+1]){
                        //biggerIndex总是记录较大子节点的索引
                        biggerIndex++;
                    }
                }
                //如果k节点的值小于其较大的子节点的值
                if(data[k]<data[biggerIndex]){
                    //交换他们
                    swap(data,k,biggerIndex);
                    //将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
                    k=biggerIndex;
                }else{
                    break;
                }
            }
        }
    }

快排

在要求时间最快时常用的一种排序方法
思路:
在这里插入图片描述

  1. 选择第一个数为p,小于p的数放在左边,大于p的数放在右边。
  2. 递归的将p左边和右边的数都按照第一步进行,直到不能递归。
public static void quickSort(int[] numbers, int start, int end) {
    if (start < end) {
        int base = numbers[start]; // 选定的基准值(第一个数值作为基准值)
        int temp; // 记录临时中间值
        int i = start, j = end;
        do {
            while ((numbers[i] < base) && (i < end))
                i++;
            while ((numbers[j] > base) && (j > start))
                j--;
            if (i <= j) {
                temp = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = temp;
                i++;
                j--;
            }
        } while (i <= j);
        if (start < j)
            quickSort(numbers, start, j);
        if (end > i)
            quickSort(numbers, i, end);
    }
}

部分代码验证:
部分代码验证

归并排序

场景:速度仅次于快排,内存少的时候使用,可以进行并行计算的时候使用。
思想:

  1. 选择相邻两个数组成一个有序序列。
  2. 选择相邻的两个有序序列组成一个有序序列。
  3. 重复第二步,直到全部组成一个有序序列
    归并
    代码1:
public static void mergeSort(int[] numbers, int left, int right) {
   int t = 1;// 每组元素个数
   int size = right - left + 1;
   while (t < size) {
       int s = t;// 本次循环每组元素个数
       t = 2 * s;
       int i = left;
       while (i + (t - 1) < size) {
           merge(numbers, i, i + (s - 1), i + (t - 1));
           i += t;
       }
       if (i + (s - 1) < right)
           merge(numbers, i, i + (s - 1), right);
   }
}
private static void merge(int[] data, int p, int q, int r) {
   int[] B = new int[data.length];
   int s = p;
   int t = q + 1;
   int k = p;
   while (s <= q && t <= r) {
       if (data[s] <= data[t]) {
           B[k] = data[s];
           s++;
       } else {
           B[k] = data[t];
           t++;
       }
       k++;
   }
   if (s == q + 1)
       B[k++] = data[t++];
   else  
       B[k++] = data[s++];
   for (int i = p; i <= r; i++)
       data[i] = B[i];
}

代码2:

public static void mergeSort(int[] arr) {
    sort(arr, 0, arr.length - 1);
}

public static void sort(int[] arr, int L, int R) {
    if(L == R) {
        return;
    }
    int mid = L + ((R - L) >> 1);
    sort(arr, L, mid);
    sort(arr, mid + 1, R);
    merge(arr, L, mid, R);
}

public static void merge(int[] arr, int L, int mid, int R) {
    int[] temp = new int[R - L + 1];
    int i = 0;
    int p1 = L;
    int p2 = mid + 1;
    // 比较左右两部分的元素,哪个小,把那个元素填入temp中
    while(p1 <= mid && p2 <= R) {
        temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    // 上面的循环退出后,把剩余的元素依次填入到temp中
    // 以下两个while只有一个会执行
    while(p1 <= mid) {
        temp[i++] = arr[p1++];
    }
    while(p2 <= R) {
        temp[i++] = arr[p2++];
    }
    // 把最终的排序的结果复制给原数组
    for(i = 0; i < temp.length; i++) {
        arr[L + i] = temp[i];
    }
}

流程示意图:
归并排序如果归并排序还能顶的住的话,下面来看看基数排序
顶得住

基数排序

场景:用于大量数,很长的数进行排序时。
思想:

  1. 将所有的数的个位数取出,按照个位数进行排序,构成一个序列。
  2. 将新构成的所有的数的十位数取出,按照十位数进行排序,构成一个序列。
    基数
public void sort(int[] array) {
        //首先确定排序的趟数;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }
        int time = 0;
        //判断位数;
        while (max > 0) {
            max /= 10;
            time++;
        }
        //建立10个队列;
        List<ArrayList> queue = new ArrayList<ArrayList>();
        for (int i = 0; i < 10; i++) {
            ArrayList<Integer> queue1 = new ArrayList<Integer>();
            queue.add(queue1);
        }
        //进行time次分配和收集;
        for (int i = 0; i < time; i++) {
            //分配数组元素;
            for (int j = 0; j < array.length; j++) {
                //得到数字的第time+1位数;
                int x = array[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
                ArrayList<Integer> queue2 = queue.get(x);
                queue2.add(array[j]);
                queue.set(x, queue2);
            }
            int count = 0;//元素计数器;
            //收集队列元素;
            for (int k = 0; k < 10; k++) {
                while (queue.get(k).size() > 0) {
                    ArrayList<Integer> queue3 = queue.get(k);
                    array[count] = queue3.get(0);
                    queue3.remove(0);
                    count++;
                }
            }
        }
    }

最后
累

应该使用那种排序算法

先关注一下各大算法的时间复杂度:

算法 是否稳定 时间复杂度 空间复杂度 备注
选择排序 n 2 n^2 n2 1
插入排序 n n n <= f ( n ) f(n) f(n) <= n 2 n^2 n2 1 取决于输入元素的排序情况
希尔排序 n l o g n nlogn nlogn or 待定 1
快速排序 n l o g n nlog n nlogn l o g n logn logn 运行效率由概率保证
归并排序 n l o g n nlogn nlogn n n n
堆排序 n l o g n nlogn nlogn 1

由表里面可以看出除了希尔排序他的复杂度是一个近似、插入和快排和输入元素的分布有着很大的关系。由大量的数学和计算机系统的实现证明快速排序是最快的通用排序算法,而且在大多数实际情况中,快速排序也是最佳的选择。当然面对多种排序算法和各式计算机系统针对不同的情况,如果要求稳定而且空间又不是问题,归并排序可能是最好的。
一些性能优先的应用重点可能是将数字排序,因此更合理的做法是跳过引用直接将原始数据进行数据排序。例如,想想将一个double类型的数组和一个Double类型的数组排序的差别。对于前者我们可以直接交换这些数并将数组排序;而对于后者,我们交换的是存储了这些数字Double对象的引用。如果我们只是将大数组排序的话,跳过引用可以为我们节省存储所有引用所需要的空间和通过引用来访问数字的成本,更不用说那些需要调用compareTo()和less()方法的开销了。所以面对以类为数据的排序我们可以直接重定义那些调用的对比函数,甚至可以把less()或者compare()这样的函数替换成a[i]<a[j]这样的代码。
对于Java而言,我们可以考虑Java系统中的主要排序方法java.util.Arrays.Sort(),根据不同的参数类型,它实际上代表了一系列排序方法:

  • 每种原始数据类型都有一个不同的排序方法
  • 一个适用于所有实现了Comparable接口的数据类型的排序方法
  • 一个适用于实现了比较器Comparator的数据类型的排序方法
    Java系统默认的是对原始数据类型使用快速排序,对引用类型使用归并排序。这些选择实际上也按时着用速度和空间来换取稳定性。

举个栗子:
现有一个购菜清单,找出其中单价或者总价排名的前两个。。。
Comparable:
可在自己类里面实现这个借口,通过对compareTo函数的改造以达到目的
FoodInfo.class

public class FoodInfo implements Comparable<FoodInfo> {
    private String name;
    private float count;
    private float price;

    public FoodInfo(String name, float count, float price) {
        this.name = name;
        this.count = count;
        this.price = price;
    }

    @Override
    public int compareTo(FoodInfo otherfoodInfo) {
        return Double.compare(price,otherfoodInfo.getPrice());
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getCount() {
        return count;
    }

    public void setCount(float count) {
        this.count = count;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }
}

调用排序:

    private void initComp2() {
        FoodInfo[] foodInfos = new FoodInfo[4];
        foodInfos[0] = new FoodInfo("a", 1.1f, 2.2f);
        foodInfos[2] = new FoodInfo("b", 1.3f, 3.4f);
        foodInfos[1] = new FoodInfo("c", 1.4f, 5.6f);
        foodInfos[3] = new FoodInfo("d", 1.5f, 7.8f);
        Arrays.sort(foodInfos);
        for (int i = 0; i < foodInfos.length; i++) {
            Log.e("SortTest", foodInfos[i].getName());
        }
    }

Comparator:

private void initComp1() {
        FoodInfo foodInfo1 = new FoodInfo("a", 1.1f, 2.2f);
        FoodInfo foodInfo3 = new FoodInfo("b", 1.3f, 3.4f);
        FoodInfo foodInfo2 = new FoodInfo("c", 1.4f, 5.6f);
        FoodInfo foodInfo4 = new FoodInfo("d", 1.5f, 7.8f);
        List<FoodInfo> foods = new ArrayList<>();
        foods.add(foodInfo1);
        foods.add(foodInfo2);
        foods.add(foodInfo3);
        foods.add(foodInfo4);
        foods.sort(comparator);
        for (FoodInfo info : foods) {
            Log.e("SortTest", info.getName());
        }
    }

    Comparator<FoodInfo> comparator=new Comparator<FoodInfo>() {
        @Override
        public int compare(FoodInfo foodInfo, FoodInfo t1) {
            return Double.compare(foodInfo.getPrice(), t1.getPrice());
        }
    };

输出结果:

2019-09-09 14:53:58.826 21601-21601/? E/SortTest: Comparable
2019-09-09 14:53:58.826 21601-21601/? E/SortTest: a
2019-09-09 14:53:58.826 21601-21601/? E/SortTest: b
2019-09-09 14:53:58.826 21601-21601/? E/SortTest: c
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: d
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: Comparator
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: a
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: b
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: c
2019-09-09 14:53:58.827 21601-21601/? E/SortTest: d

猜你喜欢

转载自blog.csdn.net/qq_33717425/article/details/97035848