【一起学算法】- 基础篇

本文介绍了算法中的时间复杂度和空间复杂度概念,包括如何估算时间复杂度,常数项优化,以及最优解的追求。文中还探讨了额外空间复杂度的计算,并通过示例展示了如何通过比较常数项来判断算法优劣。此外,文章通过代码示例解释了对数器在排序算法测试中的应用,以及异或运算在数值交换和查找特定模式数中的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


在这里插入图片描述

文章前言

  • 【一起学算法】专栏持续更新中,会在这里记录算法学习的过程
  • 文末获取【一起学算法】Github仓库手写算法源码,一起跟着写一遍吧~
  • 【一起学算法】中所涉及的部分关于leetcode中的原题均可在leetcode官网的运行器通关~

时间复杂度

什么是时间复杂度
  • 常数时间的操作:固定时间完成操作

    • 常见算术运算(+、-、*、/、%)等

    • 常见位运算(>>、>>>、<<、|、&、^)等

    • 赋值、比较、自增、自减操作等

    • 数组寻址

  • 非常数时间的操作:LinkedList寻址

时间复杂度估算
  • 等差数列求和公式:aN²+bN+c

  • 当N->无穷,常数项可忽略,只看N的最高阶,比如一个算法时间复杂度为O(N³),另一个算法时间复杂度为O(N²),两个算法干的是同一件事的情况下 O(N²) 优于 O(N³)

额外空间复杂度

  • 实现一个算法,需要开辟一些空间来支持算法流程
  • 必要的或者和实现目标有关的空间都不算做额外空间
    • 作为输入参数的空间
    • 作为输出结果的空间
  • 只需要开辟有限几个变量:额外空间复杂度为O(1)

常数项

  • 当时间复杂度相同时,通过比常数项来判断算法的优劣

  • 常数项对比

    • 放弃理论,生成随机数据进行测试(数据量*测试次数 以最终耗费时间做对比 )

最优解

  • 时间复杂度尽可能的低

  • 尽可能优化空间

常见时间复杂度排名

O(1) O(logN) O(N) O(N*logN)

O(N²) O(N³) ··· O(N^K)

O(2^N) O(3^N) ··· O(k^N)

O(N!)

对数器

import java.util.Arrays;

/**
 * @author Rhys.Ni
 * @version 1.0
 * @date 2021/12/1 3:22 上午
 * @Description 数组排序对数器
 */
public class DataTester {
    public static void main(String[] args) {
        int testCount = 500000;
        int maxSize = 100;
        int maxValue = 100;
        boolean isSuccess = true;
        for (int i = 0; i < testCount; i++) {
            int[] arr = generateRandomArray(maxSize, maxValue);
            int[] arr1 = copyArray(arr);
            int[] arr2 = copyArray(arr);
            //需要测试的排序方法
            SelectionSort.sort(arr1);
            //用系统自带排序作为对数器和手写的排序方法做对比
            comparator(arr2);
            if (!isEqual(arr1, arr2)) {
                for (int j = 0; j <arr.length ; j++) {
                    System.out.print(arr[j]+" ");
                }
                System.out.println();
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess?"Nice!":"Fucking fucked!");
    }

    public static int[] generateRandomArray(int maxSize, int maxValue) {
        // Math.random() ->  [0,1) 所有的小数,等概率返回一个
        // Math.random() * N -> [0,N) 所有小数,等概率返回一个
        // (int)(Math.random() * N) -> [0,N-1] 所有的整数,等概率返回一个
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    public static void comparator(int[] arr) {
        Arrays.sort(arr);
    }

    public static int[] copyArray(int[] arr) {
        if (arr == null) {
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    public static boolean isEqual(int[] arr1, int[] arr2) {
        if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
            return false;
        }
        if (arr1 == null && arr2 == null) {
            return true;
        }
        if (arr1.length != arr2.length) {
            return false;
        }
        for (int i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }
}

异或运算

int a=7;
int b=13;

//a ^ b = ?
//相同为0,不同为1
//a的二进制:0111
//b的二进制:1101
a ^ b = 10;
  • 异或相加又称无进位相加

  • 异或三大性质(技巧)

    • 0 ^ N = N
    • N ^ N = 0
    • 有一批数做异或运算,同时满足交换律和结合律,得到的结果是同一个数
      • 偶数个1结果一定是0
      • 奇数个1结果一定是1

题记

  • 两数值交换

    int temp = 0;
    temp = a;
    a = b;
    b = temp;
      
    //以上代码可用异或代替
    1、a = a ^ b;
    //第一步:设 a=甲 ,b=乙 得:a = 甲 ^ 乙
    //将a = 甲 ^ 乙带入第二步中
    2、b = a ^ b;
    //第二步:b = 甲 ^ 乙 ^ 乙
    //由N ^ N = 0 (乙^乙=0)得:b = 甲 ^ 0 
    //由0 ^ N = N 得 b = 甲
    //将a = 甲^乙 ,b = 甲带入第二步中
    3、a = a ^ b;
    //第三步:a = 甲^乙^甲 = 甲^甲^乙 = 0^乙 = 乙
    //至此 a = 乙 , b = 甲,完成了两数的互换
    //注意:此方法只可以用作两个不同区域的
    
    /**
     * @author Rhys.Ni
     * @version 1.0
     * @date 2021/12/3 12:47 上午
     * @Description 两值互换
     */
    public class TwoValueExchange {
        public static void main(String[] args) {
            twoValueExchange();
            twoValueExchangeForArray();
        }
    
        /**
         * 不用额外变量交换两个数的值
         */
        public static void twoValueExchange() {
            int a = 10;
            int b = 4;
            a = a ^ b;
            b = a ^ b;
            a = a ^ b;
            System.out.println("a=" + a);
            System.out.println("b=" + b);
        }
    
        /**
         * 不用额外变量交换数组中两个数的值
         */
        public static void twoValueExchangeForArray() {
            int[] arr = {1, 5, 3, 6};
            printArray(arr);
            //交换1、3位置的值
            arr[1] = arr[1] ^ arr[3];
            arr[3] = arr[1] ^ arr[3];
            arr[1] = arr[1] ^ arr[3];
            printArray(arr);
        }
      
       public static void printArray(int[] arr) {
            for (int i = 0; i < arr.length; i++) {
                System.out.print(arr[i] + " ");
            }
            System.out.println();
        }
    }
    
  • 怎么把一个int类型的数,提取出二进制中最右侧的1来

    // 运用 a & (-a) 技巧
    //设:a = 01101110010000
    //第一步:对a取反
    //得:~a = 10010001101111
    //根据 -a = ~a+1
    //得:-a = 10010001110000
    // a & (-a) = 00000000010000
    
  • 找数

    • 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数?
    • 一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
    /**
     * @author Rhys.Ni
     * @version 1.0
     * @date 2021/12/2 10:10 下午
     * @Description
     */
    public class EvenTimesOddTimes {
        public static void main(String[] args) {
            findOneKindOddNum();
            findTwoKindsOddNum();
        }
    
        //一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数?
        //利用性质三:
        // 有一批数做异或运算,同时满足交换律和结合律,得到的结果是同一个数
        // 偶数个1结果一定是0
        // 奇数个1结果一定是1
        public static void findOneKindOddNum() {
            int[] arr = {7, 4, 5, 7, 6, 7, 4, 5, 6};
            int exclusiveOr = 0;
            for (int i = 0; i < arr.length; i++) {
                exclusiveOr ^= arr[i];
            }
            // 44  55  66  777
            // 4^4=0   5^5=0   6^6=0   7^7=0  最终三个7消掉两个剩一个7直接返回
            System.out.println(exclusiveOr);
        }
    
        //一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
        public static void findTwoKindsOddNum() {
            int[] arr = {7, 4, 5, 7, 6, 7, 5, 6};
            int exclusiveOr = 0;
            for (int i = 0; i < arr.length; i++) {
                exclusiveOr ^= arr[i];
            }
            //利用 -a = ~a+1 提取最右边的1
            int oneRight = exclusiveOr & (-exclusiveOr);
            int exclusiveOr2 = 0;
            for (int i = 0; i < arr.length; i++) {
                //4:0100   5:0101  6:0110  7:0111
                //只将最右边为1的数参与异或运算,与运算性质是只有两个位上都是1结果才为1,所以两个数与运算结果!=0的时候必然							 是两种数中的其中一个
                if ((arr[i] & oneRight) != 0) {
                    exclusiveOr2 ^= arr[i];
                }
            }
            //既然已经得到两种数中的其中一种,那么另外一种就直接用 exclusiveOr ^ exclusiveOr2 即可得出
            System.out.println(exclusiveOr2 + " , " + (exclusiveOr ^ exclusiveOr2));
        }
    }
    
    
  • 一个数组中有一种数出现K次,其他数都出现了M次,已知M > 1,K < M,找到出现了K次的数,要求额外空间复杂度O(1),时间复杂度O(N)

    import java.util.HashMap;
    import java.util.HashSet;
    
    /**
     * @author Rhys.Ni
     * @version 1.0
     * @date 2021/12/3 3:08 上午
     * @Description
     */
    public class KM {
        public static void main(String[] args) {
            int kinds = 5;
            //设置需要生成几种数,至少两种数
            int maxValue = 100;
            int testTimes = 100000;
            for (int i = 0; i < testTimes; i++) {
                //-----------------------------------
                //以下代码保证了始终满足k<m条件
                int a = (int) (Math.random() * 9 + 1);
                int b = (int) (Math.random() * 9 + 1);
                int k = Math.min(a, b);
                int m = Math.max(a, b);
                if (k == m) {
                    m++;
                }
                //-----------------------------------
                int numKinds = (int) (Math.random() * kinds) + 2;
                int[] arr = randomArray(numKinds, maxValue, k, m);
                int res1 = test(arr, k);
                int res2 = findKTimesNum(arr, k, m);
                if (res1 != res2) {
                    System.out.println("出错了! " + res2);
                }
            }
            System.out.println("Nice!");
        }
    
        /**
         * 随机生成一个符合题意的数组
         *
         * @param maxValue
         * @param k
         * @param m
         * @return
         */
        public static int[] randomArray(int numKinds, int maxValue, int k, int m) {
            //首先设置一个数往数组里塞k次
            int kNum = randomNum(maxValue);
            //由k , m ,以及生成几种数决定数组长度
            // k+(numKinds-1)*m
            int[] arr = new int[k + (numKinds - 1) * m];
            int i = 0;
            for (; i < k; i++) {
                arr[i] = kNum;
            }
            //第一种数添加完成后减少一种数
            numKinds--;
            //生成的数要不能重复,所以建立hash表来记录已经存在的数
            HashSet<Integer> set = new HashSet<>();
            set.add(kNum);
            while (numKinds != 0) {
                int currNum = 0;
                do {
                    //如果hash表已经存在生成的数就重新生成;
                    currNum = randomNum(maxValue);
                } while (set.contains(currNum));
                set.add(currNum);
                numKinds--;
                //填充m个
                for (int j = 0; j < m; j++) {
                    //接着i的位置往后填充
                    arr[i++] = currNum;
                }
            }
            return upset(arr);
        }
    
    
        /**
         * 将规律的数组打乱
         *
         * @param arr
         * @return
         */
        public static int[] upset(int[] arr) {
            for (int i = 0; i < arr.length; i++) {
                int j = (int) (Math.random() * arr.length);
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
            return arr;
        }
    
        /**
         * 随机生成一个 -num ~ num的数
         *
         * @param range
         * @return
         */
        public static int randomNum(int range) {
            return (int) (Math.random() * range + 1) - (int) (Math.random() * range + 1);
        }
    
    
        /**
         * 寻找出现k次的数
         *
         * @param arr
         * @param m
         * @return
         */
        public static int findKTimesNum(int[] arr, int k, int m) {
            //设k=3;M=5  满足M > 1,K < M
            int[] initArr = new int[32];
            for (int num : arr) {
                for (int j = 0; j < initArr.length; j++) {
                    //&1的含义:j=0 时,num=0011 ,0011 & 1 = 1 initArr[0]+=1 在initArr[0]这个位置上记录一次
                    //j = 1 时,num >> 1 = 0001 ,0001 & 1 = 1 initArr[1]+=1 initArr[1]记录一次
                    //j = 2 时,num >> 2 = 0000 , 0000 & 1 = 0 initArr[1]+=0 相当于initArr[2]位置不记录
                    //以此类推,相当于从最低位到最高位挨个记录对应位置为1的次数
                    initArr[j] += (num >> j) & 1;
                }
            }
    
            //new一个32位int类型变量,遍历记录完各个数出现次数的initArr数组,用每个位上的次数对K和M取模,结果为1的说明这个        					 数在第i位上为1
            //逐位往new的变量里面设置值,最终得到一个数
            int result = 0;
            for (int i = 0; i < initArr.length; i++) {
                if (initArr[i] % m != 0) {
                    //在第几位上有1就将result的第几位设置成1
                    // 如:第2位上有1时 1(0001)<<2 = 0100 ;0100 | result(0000) = 0100
                    //    第3位上有1时 1(0001)<<3 = 1000 ;1000 | result(0100) = 1100
                    // 得到最终result为1100 (此结果为举例结果,非题解)
                    result |= (1 << i);
                }
            }
            return result;
        }
    
        /**
         * 对数器遍历计数法 百分百正确的结果
         *
         * @param arr
         * @param k
         * @return
         */
        public static int test(int[] arr, int k) {
            HashMap<Integer, Integer> map = new HashMap<>();
            for (int num : arr) {
                if (map.containsKey(num)) {
                    map.put(num, map.get(num) + 1);
                } else {
                    map.put(num, 1);
                }
            }
            for (int num : map.keySet()) {
                if (map.get(num) == k) {
                    return num;
                }
            }
            return -1;
        }
    }
    
  • 一个数组中有一种数出现K次,其他数都出现了M次,已知M > 1,K < M,找到出现了K次的数,并且当这个数没有出现K次的时候返回-1要求额外空间复杂度O(1),时间复杂度O(N)

    //findKTimesNum方法改写
    public static int findKTimesNum(int[] arr, int k, int m) {
      //设k=3;M=5  满足M > 1,K < M
      int[] initArr = new int[32];
      for (int num : arr) {
        for (int j = 0; j < initArr.length; j++) {
          //&1的含义:j=0 时,num=0011 ,0011 & 1 = 1 initArr[0]+=1 在initArr[0]这个位置上记录一次
          //j = 1 时,num >> 1 = 0001 ,0001 & 1 = 1 initArr[1]+=1 initArr[1]记录一次
          //j = 2 时,num >> 2 = 0000 , 0000 & 1 = 0 initArr[1]+=0 相当于initArr[2]位置不记录
          //以此类推,相当于从最低位到最高位挨个记录对应位置为1的次数
          initArr[j] += (num >> j) & 1;
        }
      }
    
      //new一个32位int类型变量,遍历记录完各个数出现次数的initArr数组,用每个位上的次数对K和M取模,结果为1的说明这个数在第i位上为1
      //逐位往new的变量里面设置值,最终得到一个数
      int result = 0;
      for (int i = 0; i < initArr.length; i++) {
        if (initArr[i] % m == 0) {
          continue;
        }
        if (initArr[i] % m == k) {
          result |= (1 << i);
        } else {
          result = -1;
        }
      }
    
      //解决0没有出现k次的情况
      if (result==0){
        int count = 0;
        for (int num:arr) {
          if (num==0){
            count++;
          }
        }
        if (count!=k){
          return -1;
        }
      }
      return result;
    }
    
    
    
    //randomArray方法改写
    public static int[] randomArray(int numKinds, int maxValue, int k, int m) {
      //首先设置一个数往数组里塞k次
      int kNum = randomNum(maxValue);
    
      //使生成k次的几率为50%,50%几率随机生成一个比m小的数;
      int times = Math.random()<0.5?k: (int) ((Math.random() * (m - 1)) + 1);
      int[] arr = new int[times + (numKinds - 1) * m];
      int i = 0;
      for (; i < times; i++) {
        arr[i] = kNum;
      }
    
      //第一种数添加完成后减少一种数
      numKinds--;
      //生成的数要不能重复,所以建立hash表来记录已经存在的数
      HashSet<Integer> set = new HashSet<>();
      set.add(kNum);
      while (numKinds != 0) {
        int currNum = 0;
        do {
          //如果hash表已经存在生成的数就重新生成;
          currNum = randomNum(maxValue);
        } while (set.contains(currNum));
        set.add(currNum);
        numKinds--;
        //填充m个
        for (int j = 0; j < m; j++) {
          //接着i的位置往后填充
          arr[i++] = currNum;
        }
      }
      return upset(arr);
    }
    
    
    //对数器改写
    public static void main(String[] args) {
      int kinds = 5;
      //设置需要生成几种数,至少两种数
      int maxValue = 100;
      int testTimes= 100000;
    
      for (int i = 0; i < testTimes; i++) {
        //-----------------------------------
        //以下代码保证了始终满足k<m条件
        int a = (int) (Math.random() * 9 + 1);
        int b = (int) (Math.random() * 9 + 1);
        int k = Math.min(a, b);
        int m = Math.max(a, b);
        if (k == m) {
          m++;
        }
        //-----------------------------------
        int numKinds = (int) (Math.random() * kinds) + 2;
        int[] arr = randomArray(numKinds, maxValue, k, m);
        int res1 = test(arr, k);
        int res2 = findKTimesNum(arr, k, m);
    
        if (res1 != res2) {
          System.out.println("出错了! "+res1 +" "+ res2);
        }
      }
      System.out.println("Nice!");
    }
    

    在这里插入图片描述

源码获取地址

加油小伙伴~ 点击获取算法篇代码~

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倪倪N

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值