数据结构与算法基础到高级,直击BTAJ,刷爆Letcode

🎉🎉分享DS学习笔记,不定时更新,内容对标左神全套教学~


笔记并非按照内容类型整理,而是根据一定的逻辑关系组织而成。如有问题可🔖留言评论区或📣私信哦~

🎄前序补充

🍕异或

  • 异或结果与顺序无关

XYZ = YZX = XZY

  • 应用

交换两个数的值

//a = X,b = Y
//使用前提:a、b在内存中是两块独立的空间(例如,将数组中i与j位置互换,若i=j,会将i位置值处理为0)
public static void exchange(int a,int b){
    a = a^b;	//a = X^Y , b = Y
	b = a^b;	//a = X^Y , b = X^Y^Y = X^0 = X
	a = a^b;	//a = X^Y^X = Y^0 = Y , b = X
}

一堆数字中,X出现了奇数次,其它数字出现了偶数次,求X

//将所有数值取异或,偶数次数字两两异或为0,最终剩余X^0 = X
//X^Y^V^Z^X^X^V^T^Y^T^Z = (Y^Y)^(V^V)^(Z^Z)^(T^T)^(X^X)^X = 0^X = X
public static void printOddTimesNum(int[] arr){
    int eor = 0;
    for(int cur:arr){
        eor ^= cur;
    }
    System.out.println(eor)
}

一堆数字中,X,Y(X、Y是不同的两个数)出现了奇数次,其它数字出现了偶数次,求X,Y

//将所有数值取异或,结果为eor = X^Y,由于X!=Y,因此eor != 0,即eor必然有一个位置为1(代表X、Y此位置一个为1,一个为0)。
//将此位置为1的数异或,得到X or Y中的一个数;将eor与该数异或,得到另一个数
public static void printOddTimesNum(int[] arr){
    int eor = 0,onlyOne = 0;
    for(int curNum:arr){
        eor ^= curNum;
    }
    //(与自身取反+1再相与)提取出最右的1
    //eor:		1010111100
    //~eor:		0101000011
    //~eor+1:	0101000100
    //&:		0000000100
    int rightOne = eor & (~eor+1);
    
    for(int cur:arr){
        //将最右1位置为1的数相异或
        if((cur & rightOne) == 1){
            onlyOne ^= cur;
        }
    }
    System.out.println(onlyOne+" "+(eor ^ onlyOne));
}

🍔对数器

  • 准备两个算法A和B,A为追求时间复杂度的算法(一般为待测试方法),B为不追求时间复杂度容易实现的算法,使用定量的随机数据分别代入A和B中进行运算,将结果进行比较,当测试数据集扩大到一定量时结果依旧准确,说明算法A正确。

不依赖线上OJ平台且测试数据更准确

// 以排序算法为例,准备一个随机数生成器,生成指定数量的数据,分别使用待测算法和稳定算法进行排序,排序完成后比较排序结果,若在一定的数据量下,所有数据集均测试成功,则代表待测算法通过
public static void main(String[] args){
    	// 测试数量
        int testTime = 5000000;
        int maxSize = 100;
        int maxValue = 100;
        boolean succeed = true;
        for(int i = 0; i < testTime; i++){
            // 通过随机数生成器生成指定数量的数据集
            int[] arr1 = generateRandomArray(maxSize, maxValue);
            int[] arr2 = copyArray(arr1);
            // 分别使用两种算法进行排序
            bubbleSort(arr1);
            comparator(arr2);
            // 比较排序结果
            if(!isEqual(arr1, arr2)){
                succeed = false;
                break;
            }
        }
        System.out.println(succeed ? "Pass!" : "Fail!");
    }

    public static void comparator(int[] arr){
        // 使用系统提供的排序做对数器
        Arrays.sort(arr);
    }

	// 依次比较排序结果
    public static boolean isEqual(int[] arr1, int[] arr2){
        for (int i = 0; i < arr1.length; i++){
            if(arr1[i] != arr2[i]){
                return false;
            }
        }
        return true;
    }

    // 随机数生成器
    public static int[] generateRandomArray(int maxSize, int maxValue){
        // (int)(Math.random() * N)  ->  [0,N-1]所有整数,等概率随机返回一个

        // 生成随机长度的数组
        int[] arr = new int[(int)(Math.random() * (maxSize + 1))];
        // 为数组赋值随机数
        for(int i = 0; i < arr.length; i++){
            arr[i] = (int)(Math.random() * (maxValue + 1)) - (int)(Math.random() * maxValue);
        }
        return 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 void swap(int a, int b){
	int t = a;
	a = b;
 	b = t;
}
// 额外开辟了一个t的辅助空间,因此为O(1)

public static void swapArray(int[] arr1, int[] arr2){// arr1.length == arr2.length
 	int[] arr3 = new int[arr1.length];
  	for(int i = 0, i < arr1.length; i++){
     	arr3[i] = arr2[i];
 		arr2[i] = arr1[i];
     	arr1[i] = arr3[i];
 	}
}
// 额外开辟了一个n大小的辅助空间,因此空间复杂度为O(n)

🌭时间复杂度基本概念

  • 常数操作:一个操作和样本的数据量没有关系,每次都是固定时间完成操作,叫做常数操作。

  • 时间复杂度为一个算法流程中,常数操作数量T(n)的一个指标,这个指标反映了当前算法在数据量增加时执行效率减缓的速度,常用O(读作big O)表示。具体来说,需要算出算法流程中发生了多少常数操作,进而总结成常数操作数量的表达式。

    在表达式中,只要去除系数的最高项(n相对于n2,在数据量激增的背景下,可忽略不计),例如:

    T(n) = 2n3+7n2-9 -> O(n3)

    for(int i = 0; i < n; i++){
        sum += i;
    }
    sum *=2
    // T(n)= n + 1  ->  O(n)
        
    for(int i = 0; i < n; i++){
        for(int j = 0; j <= i; j++){
            sum += j;
        }
    }
    sum ^= 3
    // T(n)= n(n-1)/2 + 1  ->  O(n^2)
    
  • 评价算法流程的好坏,优先观察时间复杂度的指标,进而分析不同数据样本下的实际运行时间,也就是“常数项”时间。

  • 对于更复杂的递归时间复杂度运算,可提前浏览递归中的Master公式内容。

  • 常见的算法时间复杂度由小到大依次为: Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn)<Ο(nk) < Ο(2n),随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低

🍿基本的排序算法的时间复杂度

✨冒泡排序/起泡排序(Bubble Sort)

  • 实现思路
    • 从第一位起,分别依次比较下标为(1,2)、(2,3)、(3,4)…的值,若前者大于后者,则交换两个数
    • 从头到尾执行完一轮后,数组中最大数被移动到数组尾部,继续进行第二轮、第三轮比较,分别将第二大、第三大的数移动到尾部
    • 总共进行n-1轮,每轮选择最大值的过程就像是水中气泡冒出的过程,逐渐浮出到水面
  • 图示:

img

  • 代码实现
public static void bubbleSort(int[] arr){
    if(arr == null || arr.length < 2){
        return;
    }

    for(int i = arr.length - 1; i > 0; i--){
        for(int j = 0; j < i; j++){
            if(arr[j] > arr[j+1]){
                swap(arr, j, j+1);
            }
        }
    }
}

// 交换两数的值
public static void swap(int[] arr, int i, int j){
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}
  • 时间复杂度

    O(n2)

✨插入排序(Insertion Sort)

  • 实现思路

    • 每次排序前0~n位置的数,第一次使0-0位置有序,第二次为0-1,依次为0-2,0-3,0-n…

    • 每次从n位置往前看,当n位置比前方数(假设为k位置的数)小,交换两位数,再比较n位置和k-1位置,重复操作,当k==0 or n位置数大于k位置数 时结束,增大范围(n+1)继续比较

      取其中一次排序做解析:

      假设0-4范围已经排序,当前排序0-5范围

      arr = [2,5,8,9,10,4,23,3]

      (1)比较4和10,4<10,交换位置

      arr = [2,5,8,9,4,10,23,3]

      (2)比较4和9,4<9,交换位置

      arr = [2,5,8,4,9,10,23,3]

      (3)比较4和8,4<8,交换位置

      arr = [2,5,4,8,9,10,23,3]

      (4)比较4和5,4<5,交换位置

      arr = [2,4,5,8,9,10,23,3]

      (5)比较4和2,4>2,0-5范围排序完成,继续排序0-6范围…

  • 图示:

img

  • 代码实现
public static void insertionSort(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }

        // 分别实现0~i的有序排列
        for(int i = 1; i < arr.length; i++){
            for(int j = i - 1; j >=0 && arr[j] > arr[j + 1]; j--){
                swap(arr, j, j+1);
            }
        }

    }

    // 交换两个数的值
    public static void swap(int[] arr, int i, int j){
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }
  • 时间复杂度:对于插入排序,数据状况不同,时间复杂度不同

7654321 => O(n2)

1234567 => O(n)

时间复杂度修正,按照最差情况估计,因此时间复杂度为O(n2)

🧂递归行为的时间复杂度

  • 递归实质是使用系统栈实现的过程

  • Master公式——>计算递归时间复杂度

T(N) = a * T(N/b) + O(N^d)

T(N):父问题的规模

a:子问题执行的次数

T(N/b):子问题的规模

O(N^d):剩余部分的时间复杂度

logba < d —— O(Nd)

logba > d —— O(Nlogba)

logba == d —— O(Nd*logN)

  • 应用

用递归方法查找数组最大值

时间复杂度:T(n) = 2 * T(n / 2) + O(1) --> a = 2, b = 2, d = 0 --> O(n<sup>log<sub>b</sub>a</sup>) = O(n)

public static int getMax(int[] arr){
    return process(arr, 0, arr.length - 1);
}
// arr[L...R]范围上求最大值
public static int process(int[] arr, int L, int R){
    if(L == R){  // arr[L...R]范围上只有一个数,直接返回 
        return arr[L];
    }
    int mid = L + ((R - L) >> 1);  // 中点
    int leftMax = process(arr, L, mid);
    int rightMax = process(arr, mid+1, R);
    return Math.max(leftMax, rightMax);
}

✨二分法

  • 二分法更像是一种思想,将数据量一分为二,只关注需要的部分,避免多余的操作,由于其时间复杂度非常低,因此在实际应用中使用频率比较高
  • 优化

对于中值mid = (L+R)/2,L+R可能会溢出,因此优化为mid = L+(R-L)/2

int mid = L + ((R - L) >> 1)	// >>比/的运行速度快
  • 图示

请添加图片描述

  • 应用

在一个有序数组中,找某个数是否存在,若存在返回其下标位置,否则返回-1

  public static int binarySearch(int[] arr, int aim, int L, int R){
          // 被查找数不存在,返回-1
          if(aim < arr[L] || aim > arr[R]){
              return -1;
          }
          // 中点值
          int mid = L + ((R - L) >> 1);
          // 中点为目标查找数,返回下标
          if(aim == arr[mid]){
              return mid;
          }else if(aim > arr[mid]){
              // 目标数在右侧,对右侧使用二分法
              return binarySearch(arr, aim, mid + 1, R);
          }else{
              // 目标数在左侧,对左侧使用二分法
              return binarySearch(arr, aim, L, mid - 1);
          }
      }
// 时间复杂度:T(n) = 1 * T(n / 2) + O(1)  -->  = O(logn)

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

// 该问题与上一题目相似,不同点在于上一题目查找到即退出,该问题中,应该坚持查找到最后一个数
public static int binarySearch(int[] arr, int aim, int L, int R){
     int mid = L + ((R - L) >> 1);
     // 查找到最后一个数,返回下标
     if(L == R){
         return mid;
     }
     // aim在中点的左侧或aim为中点,继续向左侧查找(aim在数组中可能出现多次,因此即使查找到aim也应继续查找,以保证找到最左侧位置)
     if(arr[mid] >= aim){
         return binarySearch(arr, aim, L, mid - 1);
     }else{
         return binarySearch(arr, aim, mid + 1, R);
     }
 }
// 时间复杂度:T(n) = 1 * T(n / 2) + O(1)  -->  = O(logn)

局部最小值:在一个无序数组中,相邻两数不相等,有两种情况记为局部最小:

(1)第一个数小于第二个数或最后一个数小于倒数第二个数

(2)对于中间的任何一个数,需要满足a[i-1] > a[i] and a[i] < a[i+1]

要求找出一个局部最小值

// (消除刻板印象)二分法并非排序才能使用
// 1.对于第一种情况的局部最小值直接判断
// 2.对于第二种情况:取中值判断是否为局部最小值,若不是,选择数值小于M的一侧(因为相邻两数不相等,故一定至少存在一个局部最小值)继续二分
public static void main(String[] args) {
    int arr[] = {7, 3, 1, 4, 5, 6, 7, 8, 9};
    int R = arr.length - 1;
    // 判断边界局部最小值
    if (arr[0] < arr[1]) {
        System.out.println(0);
    } else if (arr[R] < arr[R - 1]) {
        System.out.println(R);
    } else {
        System.out.println(localMinimum(arr, 0, R));
    }
}

public static int localMinimum(int[] arr, int L, int R) {
    // 非边界局部最小值的判断
    int mid = L + ((R - L) >> 1);
    // mid为局部最小值
    if (arr[mid] < arr[mid + 1] && arr[mid] < arr[mid - 1]) {
        return mid;
    } else if (arr[mid] > arr[mid - 1]) {
        return localMinimum(arr, L, mid - 1);
    } else {
        return localMinimum(arr, mid + 1, R);
    }
}
// 时间复杂度:T(n) = 1 * T(n / 2) + O(1)  -->  = O(logn)

✨归并排序

  • 排序过程

    • 将待排序数组A分为两部分,中点记为M,左侧、右侧分别记为数组L、R,将L、R进行排序(递归实现)

    • 归并过程:构建辅助数组A'(大小为A的大小),将指针分别指向L和R的最左侧元素,比较两元素的大小,将较小者添加到A'中同时指针后移

    • 重复操作直至L或R越界(代表前半元素排序完毕),将A中剩余元素添加到A'中即可

    • 将A'赋值给A

  • 图示

img

  • 代码实现
// 处理数组,保证数组递归到两个单独的数字
    public static void progress(int[] arr, int L, int R){
        // 保证progress一直分割到一个一个数时,左右progress进行merge
        if(L == R){
            return;
        }

        int M = L + ((R - L) >> 1);
        progress(arr, L, M);
        progress(arr, M + 1, R);
        merge(arr, L, M, R);
    }

    public static void merge(int[] arr, int L, int M, int R){
        // 构建辅助数组,注意数组大小不为arr.length
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;
        while(p1 <= M && p2 <= R){
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while(p1 <= M){
            help[i++] = arr[p1++];
        }
        while(p2 <= R){
            help[i++] = arr[p2++];
        }
        for(i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
    }
// 时间复杂度:T(n) = 2 * T(n / 2) + O(n)  -->  = O(nlogn)
  • 应用

    小和问题

    在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

    例子:[1,3, 4,2, 5] -> 1左边比1小的数,没有; 3左边比3小的数,1; 4左边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、2;所以小和为1+1+3+1+1+3+4+2=16

    // 将左侧比自身小的数相加可以转换为:将各数自身的k倍相加(k为右侧比自己大的数的数量)
    public static int smallSum(int[] arr){
            if(arr.length < 2 || arr == null){
                return 0;
            }
            return progress(arr, 0, arr.length - 1);
        }
    
        public static int progress(int[] arr, int L, int R){
            if(L == R){
                return 0;
            }
    
            int M = L + ((R - L) >> 1);
    		// 注意三组值分别相加,progress分别表示左右侧数组小和数,merge表示两侧数组合并后产生的新的小和数量
            return progress(arr, L, M) + progress(arr, M + 1, R) + merge(arr, L, M, R);
        }
    
        public static int merge(int[] arr, int L, int M, int R){
            int[] help = new int[R - L + 1];
            int i = 0;
            int p1 = L;
            int p2 = M + 1;
            int res = 0;
            while(p1 <= M && p2 <= R){
                // 将右侧比自己大的数的数量*自身大小 即为 该数在当前数组中产生的小和,例如:
                // 左侧:2 4 5,右侧:6 9 14。4当前范围中产生的小和为3*4=12(当左右合并后,4不会与右侧再次进行比较,故不必担心小和会重复产生)
                // 此方法避开了遍历循环,巧妙地将时间复杂度O(n) -> O(1)
                res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 0;
                help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
            }
            while (p1 <= M){
                help[i++] = arr[p1++];
            }
            while (p2 <= R){
                help[i++] = arr[p2++];
            }
            for (i = 0; i < help.length; i++){
                arr[L + i] = help[i];
            }
            return res;
        };
    // 时间复杂度:T(n) = 2 * T(n / 2) + O(n)  -->  = O(nlogn)
    

    ✨荷兰国旗问题

    • 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
    // 将左侧划分出小于等于的区域(LE区),依次从最开始遍历元素,当元素小于等于num值,将元素与LE区下一个元素互换,同时LE区边界+1(意为将元素划入LE区),当元素大于num值,跳过,进行下一元素的判断
    public static void netherLandsFlag(int[] arr, int aim){
            // p代表小于等于num区域的边界下标
            int p = -1;
            // arr[i]<=num,将arr[i]和小于等于区域的下一个数交换,同时区域范围扩大1
            for (int i = 0; i < arr.length; i++){
                if(arr[i] <= aim){
                    int temp = arr[i];
                    arr[i] = arr[++p];
                    arr[p] = temp;
                }
            }
        }
    

    ✨快速排序

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Insight.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值