数据结构与算法面试

计算模板

进制转换

    //将b进制的数转换为十进制
    public static int get(String s, int b){
        int res = 0;
        for (char c : s.toCharArray()){
            res = res * b + c - '0';
        }

        return res;
    }

快速幂

    //预处理出 a^2^0,a^2^1,a^2^2,...,a^2^logk 这k个数
    //将a^k用这b种数来组合,即a^k = a^2^x1 * a^2^x2 * ... * a^2^xt = a^(2^x1 + 2^x2 + ... + 2^xt)  
    public double quickMi(double a, long k){
        double res = 1;
        while(k > 0){
            //如果k的二进制表示的第0位为1, 则乘上当前的a
            if((k & 1) == 1) res *= a;
            //右移一位
            k >>= 1;
            //更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
            a *= a;
        }

        return res;
    }

质数

    public boolean isPrime(int x) {
        for (int i = 2; i * i <= x; ++i) {
            if (x % i == 0) {
                return false;
            }
        }
        return true;
    }

//[2,n)的所有质数,  若[2,n] 则 i <= n  i * res.get(j) <= n
    public int countPrimes(int n) {
        boolean[] vis = new boolean[n + 1];
        List<Integer> res = new ArrayList<>();
        for(int i = 2; i < n; i++){
            if(!vis[i]) res.add(i);
            for(int j = 0; i * res.get(j) < n; j++){
                vis[i * res.get(j)] = true;
                if(i % res.get(j) == 0) break;
            }
        }

        return res.size(); 
    }

基础算法

前缀

        int[] a = new int[N];
        int[] s = new int[N];

        //原数组,下标从1开始
        for (int i = 1; i <= n; i++)
            a[i] = input.nextInt();

        //s[i]代表 a的前i项和
        for (int i = 1; i <= n; i++)
            s[i] = s[i - 1] + a[i];

        while(m-- > 0){
            int l = input.nextInt();
            int r = input.nextInt();
            //[l,r]中的和
            System.out.println(s[r] - s[l - 1]);
        }

差分

        int[] s = new int[n + 2];
        int[] b = new int[n + 2];
        //存放原数组
        for (int i = 1; i <= n; i++){
            s[i] = input.nextInt();
        }

        //构建差分数组
        for (int i = 1; i <= n; i++){
            b[i] = s[i] - s[i-1];
        }

        for (int i = 0; i < m; i++){
            int l = input.nextInt();
            int r = input.nextInt();
            int c = input.nextInt();
            //将l和以后加c, 将r之后-c
            b[l] += c;  b[r + 1] -= c;
        }

        for (int i = 1; i <= n; i++){
            //将差分改为原数组
            b[i] += b[i - 1];
        }

    	//当前的坐标
        int now = 0;
        // 求多少个单位至少被涂抹2层油漆
        while (n-- != 0){
            //当前移动的距离
            int dis = input.nextInt();
            char ch = input.next().charAt(0);
            if (ch == 'R'){
                //map[now]++;
                map.put(now,map.getOrDefault(now,0) + 1);
                //map[now+dis]--;
                map.put(now + dis,map.getOrDefault(now + dis,0) - 1);
                //向右移动,坐标变化
                now += dis;
            }else {
                //向右移动
                //map[now-dis]++;
                map.put(now - dis,map.getOrDefault(now - dis,0) + 1);
                //map[now]--;
                map.put(now,map.getOrDefault(now,0) - 1);
                //向左移动,坐标变化
                now -= dis;
            }
        }

        //res表示答案, sum表示前缀和, last表示上一次的位置
        int res = 0, sum = 0, last = 0;
        for (int x : map.keySet()){
            //[x-last]表示坐标之前的距离 (TreeMap已经排序过)
            if (sum >= 2) res += (x - last);
            //每一次加上下一点的下标的value的值,如果刷过两遍的区间,求出长度的个数
            sum += map.get(x);
            last = x;
        }

        System.out.println(res);

二分

        //大于等于x的第一个位置
        int l = 0, r = n - 1;
        while (l < r){
            int mid = l + r >> 1;
            if (a[mid] < x) l = mid + 1;
            else r = mid;
        }

        //大于x的第一个位置
        l = 0; r = n - 1;
        while (l < r){
            int mid = l + r >> 1;
            if (a[mid] <= x) l = mid + 1;
            else r = mid;
        }

对二分不同版本的理解

假设目标值在 [l,r] 中, 假设 M 是我们最后要的答案,那么这个区间就会被 M 分成两个部分

在判断的时候将 M 放在左半部分还是右半部分,决定着代码会是一个怎么的样子, 而这两个模板就是针对这两种情况而提供的

int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) l = mid + 1;
        else r = mid;
    }
    return l;
}

版本一是将区间 [l,r] 划分成 [l,mid][mid+1,r] 时(即 M 属于右半部分的时候),其更新操作是 r = mid 或者

l = mid+1,计算 mid 时不需要加 1

这个模板中为什么 l = mid + 1呢?

因为当给 l 赋值的时候,是 mid不在区间的右半部分的时候。又因为答案 M 是在右半部分,所以可以知道 mid 一定不是我们要的答案,因此 l = mid + 1(满足区间相邻两个数差值为 1时的情况下 +1)

int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

版本一是将区间 [l,r] 划分成 [l,mid - 1][mid,r] 时(即 M 属于左半部分的时候),其更新操作是 r=mid - 1或者 l = mid,此时为了防止死循环,计算 mid 时需要加 1

对于为什么 r = mid − 1 ,当给 r 赋值的时候,是 mid不在区间的左半部分的时候。又因为答案 M 是在左半部分,所以可以知道 mid 一定不是我们要的答案,因此 r = mid − 1(这里是满足区间相邻两个数差值为 1时的情况下 +1)

那 mid = l + r + 1 >> 1是怎么回事呢?

若 l + 1 = r 时,我们采用 mid = l + r >> 1 会出现一个什么样的情况?

∵mid = l + r = (2 ∗ l + 1 ) / 2 = l
∴l = mid = l
∴区间范围依然是[l,r]
所以为了出现这种死循环的情况,计算 mid = l + r + 1 >> 1

数据结构

kmp

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

排序算法

image-20220329001723587

快速排序

思路:将待排序的序列分成前后两部分,其中前一部分的数据都比后一部分的数据要小,然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序

时间复杂度:整体时间复杂度 O(n log n) ,如果运气不好,每次都选择最小值作为基准值,那么每次都需要把其他数据移到基准值的右边,递归执行 n 行,运行时间也就成了 O(n^2)

public static void quickSort(int []arr,int left,int right){
        if (left >= right)
            return;

        int i = left - 1, j = right + 1, x = arr[left + right >> 1];
        while (i < j){
            do i++; while (arr[i] < x);
            do j--; while (arr[j] > x);
            if (i < j){
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
        quickSort(arr, left, j);
        quickSort(arr,j+1,right);
    }

堆排序

思路:首先将待排序的数组构造成一个大根堆(升序用大根堆,降序就用小根堆),即整个数组的最大值就是堆结构的顶端。将顶端的数与末尾的数交换,即末尾的数为最大值,剩余待排序数组个数为 n - 1。将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
时间复杂度: O(n log n)

public class HeapSort {
    public static void heapSort(int[] arr){
        // 构造大根堆
        heapInsert(arr);
        int size = arr.length;
        while (size > 1){
            // 固定最大值
            swap(arr,0,size - 1);
            size--;
            // 构造大根堆
            heapify(arr,0,size);
        }
    }

    // 构造大根堆 (通过新插入的数上升)
    public static void heapInsert(int[] arr){
        for (int i = 0; i < arr.length; i++){
            // 当前插入的索引
            int index = i;
            // 父节点索引
            int father = (index - 1) / 2;
            // 如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点
            // 然后继续和上面的父结点值比较,直到不大于父结点,则退出循环
            while (arr[index] > arr[father]){
                // 交换当前结点与父结点的值
                swap(arr,index,father);
                // 将当前索引指向父索引
                index = father;
                // 重新计算当前索引的父索引
                father = (index - 1) / 2;
            }
        }
    }

    // 将剩余的数构造成大根堆(通过顶端的数下降)
    public static void heapify(int[] arr, int index, int size){
        int left = index * 2 + 1, right = index * 2 + 2;
        while (left < size){
            int largestIndex;
            // 判断孩子中较大的值的索引 (要确保右孩子在size范围之内)
            if (arr[left] < arr[right] && right < size){
                largestIndex = right;
            }else {
                largestIndex = left;
            }
            // 比较父结点的值与孩子中较大的值,并确定最大值的索引
            if (arr[index] > arr[largestIndex]){
                largestIndex = index;
            }
            // 如果父结点索引是最大值的索引,那已经是大根堆了,则退出循环
            if (index == largestIndex){
                break;
            }
            // 父结点不是最大值,与孩子中较大的值交换
            swap(arr,largestIndex,index);
            //将索引指向孩子中较大的值的索引
            index = largestIndex;

            // 重新计算交换之后的孩子的索引
            left = index * 2 + 1;
            right = index * 2 + 2;
        }
    }
    
    public static void swap(int[]arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

归并排序

将待排序的序列 分成 长度相同的两个子序列 ,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。该操作会一直重复执行,直到所有子序列都归并为一个整体为止
无论哪一行都是 n 个数据,所以每行的运行时间都为 O(n)。而将长度为 n 的序列对半分割直到只有一个数据为止时,可以分成 log2n 行,因此,总共有 log2n 行。也就是说,总的运行时间为 O(nlogn)

    public static void mergeSort(int []arr,int left,int right) {
        if (left >= right) return;

        int mid = left + right >> 1;
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);

        int[] temp = new int[right - left + 1]; // 临时数组, 用于临时存储 [l,r]区间内排好序的数据
        int k = 0, i = left, j = mid + 1;    //两个指针
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j])
                temp[k++] = arr[i++];
            else
                temp[k++] = arr[j++];
        }

        while (i <= mid) temp[k++] = arr[i++];
        while (j <= right) temp[k++] = arr[j++];

        //进行赋值
        for (i = left,j = 0;i <= right;i++,j++)
            arr[i] = temp[j];
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值