复杂度和简单排序算法

认识时间复杂度

常数时间的操作

  • 一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作
    • 例如 int num = arr[i];中不管arr数组中有多少数据,每次赋值都是根据索引一次查询,都是固定时间内完成,是常数操作
    • 而假如有链表list int num = list.get[i];中获取链表中的元素要逐个遍历,若获取第10万个数据需要10万次操作,所以不是常数操作

时间复杂度

  • 选择排序来举例,选择排序是每一轮找到最小值,再与最前面的值交换。数组长度为N,总共下来会有多少次常数操作:
    • 拿到数组中的元素是一次常数操作 如arr[i] ,一轮排序中每个元素都会拿到,所以是N次
    • 比较两个元素也是一次常数操作 ,一轮中每个元素都会比较一次,也是N次
    • 找到最小的元素后,要与最前面的元素交换,一轮会交换 1次
    • 总的下来:
      • 获取元素:N + N-1 + N-2 + ...
      • 进行比较:N + N-1 + N-2 + ...
      • 进行交换:1 + 1 + 1 + ... = N 次
      • 总共加起来 可以写成 aN² + bN + c 次
      • 其中 去掉低次项,去掉最高次项的系数也就是 N²
      • 时间复杂度为 O(N²)

简单排序算法

选择排序  

  • 前面已经说了选择排序的具体过程,直接看算法实现
public static void selectSort(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) { //  i~N-1 找最小值下标
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }

 冒泡排序

  •  挨个比较,后者比前者大,则交换位置,最终一轮过后,最大的会被放到最后面,如同冒泡般冒出来
  • 和选择排序一样,算法时间复杂度也为 O(N²)
  • 代码实现:
/**
     *  小数往前冒
     * @param arr
     */
    public static void bubbleSort_0(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            for (int j = arr.length - 1; j > i; j--) {
                if(arr[j] < arr[j - 1]){
                    int temp = arr[j];
                    arr[j] = arr[j-1];
                    arr[j-1] = temp;
                }
            }
        }
    }
    /**
     *  大数往后冒
     * @param arr
     */
    public static void bubbleSort_1(int[] arr){
        for (int i = arr.length - 1; i > 0; i--) {
            for(int j = 0; j < i; j++){
                if(arr[j] > arr[j + 1]){
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j+1] = temp;
                }
            }
        }
    }

 异或运算

  • 按位运算,相同为0,相异为1,也可以理解为无进位相加
  • 异或性质:
    • 1) 0 ^ N = N    N ^ N = 0
    • 2)满足交换律和结合律
    • 推导出以下代码可以交换两数的值
    • // 令 a = 甲; b = 乙;
      a = a ^ b;  //  a = 甲 ^ 乙 
      b = a ^ b;  //  b = 甲 ^ 乙 ^ 乙 = 甲 ^(乙 ^ 乙)= 甲
      a = a ^ b;  //  a = 甲 ^ 乙 ^ 甲 = (甲 ^ 甲)^ 乙 = 乙
      // 最终 a = 乙,b = 甲;完成了两数的互换 
    •  注意:该异或运算是对内存地址进行交换(数值相同也可以完成互换),若内存地址相同运算结果会为0;
      • 在数组的交换中慎用!(交换i和j位置时,i不能等于j)

例题:

  • 一个数组中,只有一种数出现了奇数次,其他数都是偶数次,求这个奇数次的数。
    • 利用异或的两个性质:交换律结合律以及 n ^ n = 0, 0^ n = n;
    • 也就是说如果整个数组全部异或起来,出现偶数次的数最终都会变成0(通过结合律与交换律),最终留下的就只有奇数次的数
int eor = 0;
for(int i = 0; i < arr.length; i ++){
    eor ^= arr[i];
}
System.out.println(eor);
  •  一个数组中,有两种数出现了奇数次,其他的数都是偶数次,求这两个数。
    • 假设两个数是a和b,同上一题一样,全部异或一遍之后 会得到 eor = a ^ b;
    • 又因为 a 不等于 b,所以 eor 的值一定不等于 0 ,eor的二进制32位中,一定有某位是为1的(不可能全0)假设为第n位
    • 说明第 n 位上,要么 a=0,b=1,要么a=1,b=0;
    • 数组中第 n 位为 1 的数就有 a、b二者之一,以及偶数个第 n 位为 1 的其他数
    • 再定义一个 eor2 = 0;仅遍历异或数组中数据的第n位为 1 数
    • 就能够得到 a 或者 b 既 eor2 = a 或者 eor2 = b;
    • eor ^ eor2 即为剩下的另一个数

 

int eor = 0;
for(int i = 0; i < arr.length; i ++){
    eor ^= arr[i];
}
// eor = a ^ b
// eor != 0  eor 必然有一个位置上是1 (二进制)
 
int rightOne = eor & (~eor + 1); // 位运算中提取出最右边的一:取反加一再相与

int eor2 = 0;
for(int i = 0; i < arr.length; i++){
    if(arr[i] & rightOne == 0){
        eor2 ^= arr[i];
    }
}
System.out.println(eor2 + " " + eor ^ eor2);

 

 

插入排序

  • 插入排序要求每轮保证 0 ~ i 范围内有序,先0~1范围内有序,0~2范围内有序......直到整个都有序
  • 具体做法在 0~i-1 已经有序的情况下,拿第 i 个数从后往前依次比较,如果 i 较小则交换,i 较大说明当前位置为正确的位置,即可保证 0 ~ i 范围内有序 
  • 时间复杂度也为O(N²),但是在最好的情况下可以达到O(N)
  • 而选择排序和冒泡排序是严格的O(N²)
  • 在基本有序的情况下,插入排序效率是最高的
  • 代码实现:

  

    /**
     * 插入排序
     * @param arr
     */
    public static void insertSort(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        // 0~0 肯定有序
        // 直接从1开始
        for (int i = 1; i < arr.length; i++) {
            for (int j = i; j > 0; j--) {
                if(arr[j] < arr[j-1]){
                    int temp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = temp;
                }else{
                    break;
                }
            }
        }
    }

二分法

  • 时间复杂度O(log2N)

例:在一个有序数组中找某个数是否存在

  • 前提是有序,先找出中间数,如果该数比中间数大,那么该数一定不在左边,然后再找右边的中间数,依次下去

例:在一个有序数组中,找>=某个数最左侧的位置

  • 如例1,同样先二分,如果中间数满足>=要找的数,用一个变量t存储这时的下标,取左边 继续二分,如果不满足,则取右边二分。
  • 继续二分之后得到的中间数如果也满足,且下标比t更小,则替换t的值。
  • 最终一定能找到最靠左侧的值的位置
  • 与题1不同的是,本题一定要二分到底,不会提前结束

例:无序数组,没有相等的数,局部最小值问题

  • 最左侧是数如果小于其右边一个的数,则它是局部最小值,最右侧的数如果小于其左边的一个数,则它是局部最小值,中间的数要比两边的两个数都小,才是局部最小值,现在要寻找一个局部最小值。
  • 无序也有二分法可以利用的时候
  • 先判断两端的数,如果满足局部最小直接返回
  • 两端不满足的情况下,一定是两端的大于内侧的数,两端数据的趋势都是向内递减
  • 从一端到另一端要满足这种趋势,中间则一定存在局部最小值
  • 二分法取中间值,判断其与两边的大小情况,若小于两边,则返回该值
  • 若大于左边,则和数组右端的趋势一样,则左半数据一定存在局部最小值
  • 若大于右边,则和数组左端的趋势一样,则右半数据一定存在局部最小值
  • 依次下去,一定能找到一个局部最小值

 

posted @ 2023-11-03 01:59  风陵南  阅读(30)  评论(0)    收藏  举报