面试题38、39、40、41

面试题38.字符串的排列

在这里插入图片描述

一般回溯法
class Solution {
    public String[] permutation(String s) {
        List<String> res = new ArrayList<>();
        char[] ch = s.toCharArray();
        Arrays.sort(ch);
        StringBuilder sb = new StringBuilder();
        //标记哪个字符正在被使用,哪个没有使用
        boolean[] used = new boolean[ch.length];
        dfs(ch, 0, sb, used, res);
        String[] arr = new String[res.size()];
        for(int i = 0; i < arr.length; i++) {
            arr[i] = res.get(i);
        }
        return arr;
    }
    public void dfs(char[] ch, int index, StringBuilder sb, boolean[] used, List<String> res) {
        if(index >= ch.length) {
            res.add(sb.toString());
            return;
        }
        for(int i = 0; i < ch.length; i++) {
            if(used[i]) continue; 
            //剪枝
            //使结果数组中没有重复元素
            if(i > 0 && ch[i] == ch[i - 1] && !used[i - 1]) continue;
            used[i] = true;
            sb.append(ch[i]);
            dfs(ch, index + 1, sb, used, res);
            used[i] = false;
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

去重回溯法

根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第 1 位字符( n 种情况)、再固定第 2 位字符( n−1 种情况)、… 、最后固定第 n 位字符( 1 种情况)。

在这里插入图片描述
当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” 。

在这里插入图片描述

  • 1、终止条件: 当 x = len( c ) - 1 时,代表所有位已固定(最后一位只有 1 种情况),则将当前组合 c 转化为字符串并加入 res ,并返回;

  • 2、递推参数: 当前固定位 x ;

  • 3、递推工作: 初始化一个 Set ,用于排除重复的字符;将第 x 位字符与 i ∈ [x, len( c )] 字符分别交换,并进入下层递归;

    • 剪枝: 若 c[i] 在 Set​ 中,代表其是重复字符,因此 “剪枝” ;
    • 将 c[i] 加入 Set​ ,以便之后遇到重复字符时剪枝;
    • 固定字符: 将字符 c[i] 和 c[x] 交换,即固定 c[i] 为当前位字符;
    • 开启下层递归: 调用 dfs(x + 1) ,即开始固定第 x + 1 个字符;
    • 还原交换: 将字符 c[i] 和 c[x] 交换(还原之前的交换);
class Solution {
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
		c = s.toCharArray();
		dfs(0);
		return res.toArray(new String[0]);
    }
    public void dfs(int x) {
    	if(x == c.length - 1) {
    		res.add(String.valueOf(c));
    		return;
    	}
    	Set<Character> set = new HashSet<>();
    	for(int i = x; i < c.length; i++) {
    		if(set.contains(c[i]) continue; //重复,剪枝
    		set.add(c[i]);
    		swap(i, x); // 交换,将 c[i] 固定在第 x 位
    		dfs(x + 1); // 开启固定第 x + 1 位字符
    		swap(x, i); // 恢复交换
    	}
    }
    public void swap(int a, int b) {
    	char tmp = c[a];
    	c[a] = c[b];
    	c[b] = tmp;
    }
}
  • 时间复杂度 O(N!N) : N 为字符串 s 的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为 N×(N−1)×(N−2)…×2×1 ,即复杂度为 O(N!);字符串拼接操作 join() 使用 O(N) ;因此总体时间复杂度为 O(N!N) 。
  • 空间复杂度 O(N2) : 全排列的递归深度为 N ,系统累计使用栈空间大小为 O(N) ;递归中辅助 Set 累计存储的字符数量最多为 N+(N−1)+…+2+1=(N+1)N/2 ,即占用 O(N2) 的额外空间。

—————————————————————————————

面试题39.数组中出现超过一半的数字

在这里插入图片描述

1、排序法

出现次数最多的元素大于n/2次的就是众数,可以先排序,然后下标是n/2的元素一定是众数,n为奇数或者偶数都可以。

class Solution {
    public int majorityElement(int[] nums) {
        Arrays.sort(nums);
        return nums[nums.length/2];
    }
}
  • 时间复杂度:O(nlogn)。将数组排序的时间复杂度为 O(nlogn)。
  • 空间复杂度:O(logn)。如果使用语言自带的排序算法,需要使用 O(logn) 的栈空间。如果自己编写堆排序,则只需要使用 O(1) 的额外空间。

2、map法

class Solution {
    public int majorityElement(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        int len = nums.length / 2;
        for(int num : nums) {
        	map.put(num, map.getOrDefault(num, 0) + 1);
        	if(map.get(num) > len) return num;
        }
        return 0;
    }
}
  • 时间复杂度O(n),其中 n 是数组 nums 的长度。遍历数组 nums 一次,对于 nums 中的每一个元素,将其插入哈希表都只需要常数时间。如果在遍历时没有维护最大值,在遍历结束后还需要对哈希表进行遍历,因为哈希表中占用的空间为 O(n),因此总时间复杂度为 O(n)。
  • 空间复杂度O(n)。哈希表最多包含 n - ⌊n/2⌋ 个键值对,所以占用的空间为 O(n)。这是因为任意一个长度为 n 的数组最多只能包含 n 个不同的值,但题中保证 nums 一定有一个众数,会占用(最少)⌊n/2⌋ + 1 个数字。因此最多有 n - (⌊n/2⌋ + 1) 个不同的其他数字,所以最多有n - ⌊n/2⌋ 个不同的元素。

3、摩尔投票法

推论一:若记众数的票数为 +1,非众数的票数为 -1,则一定有所有数字的票数和 > 0

推论二:若数组的前 a 个数字的票数和 = 0,则数组剩余(n - a)个数字的票数和一定仍 > 0,即后 (n-a) 个数字的 众数仍为 x 。
在这里插入图片描述

class Solution {
    public int majorityElement(int[] nums) {
		int x = 0, votes = 0;
		for(int num : nums) {
			//当票数=0,说明前面的数字都已经抵消,剩下的数字中多的那个数还是众数
			//所以不妨先假设当前数字就是众数
			if(votes == 0) x = num;
			//如果当前数字等于目前的众数,则票数+1,否则票数-1
			votes += x == num ? 1 : -1;
		}
		return x;
    }
}

位运算

如果一个数字的出现次数超过了数组长度的一半, 那么这个数字二进制的各个bit的出现次数同样超过了数组长度的一半

class Solution {
    public int majorityElement(int[] nums) {
		int[] bit = new int[32];
		int len = nums.length;
		for(int num : nums) {
			for(int i = 0; i < 32; i++) {
				//如果当前数字的当前位为1,则在数组中相应的位置上值+1
				if(((num >>> i) & 1) == 1) bit[i]++; 
			}
		}
		int res = 0;
		for(int i = 0; i < 32; i++) {
			if(bit[i] > len / 2) {
				res = res | (1 << i);
			}
		}
		return res;
    }
}

————————————————————————————————————————

面试题40.最小的k个数

在这里插入图片描述

方法一:堆

使用一个大小为 k 的最大堆,将数组中的元素依次入堆,当堆的大小超过 k 时,便将多出的元素从堆顶弹出。
在这里插入图片描述
这样,由于每次从堆顶弹出的数都是堆中最大的,最小的 k 个元素一定会留在堆里。这样,把数组中的元素全部入堆之后,堆中剩下的 k 个元素就是最大的 k 个数了。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
		if(k == 0) return new int[0];
		Queue<Integer> heap = new PriorityQueue<>(k, (i1, i2) -> Integer.compare(i2, i1));
		for(int i : arr) {
			//当前数字小于堆顶元素才会入堆
			if(heap.isEmpty() || heap.size() < k || i < heap.peek()) {
				heap.offer(i);
			}
			if(heap.size() > k) heap.poll(); //删除堆顶最大元素
		}
		//将堆中的元素存入数组
		int[] res = new int[heap.size()];
		int j = 0;
		for(int i : heap) {
			res[j++] = i;
		}
		return res;
    }
}

方法二:快排变形

我们的目的是寻找最小的 k 个数。假设经过一次 partition 操作,枢纽元素位于下标 m,也就是说,左侧的数组有 m 个元素,是原数组中最小的 m 个数。那么:

  • 若 k = m,我们就找到了最小的 k 个数,就是左侧的数组;
  • 若 k < m ,则最小的 k 个数一定都在左侧数组中,我们只需要对左侧数组递归地 parition 即可;
  • 若 k > m,则左侧数组中的 m 个数都属于最小的 k 个数,我们还需要在右侧数组中寻找最小的 k−m 个数,对右侧数组递归地 partition 即可。
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(arr.length <= k) return arr;
        quickSort(arr, k, 0, arr.length - 1);
        int[] res = new int[k];
        for(int i = 0; i < k; i++) {
            res[i] = arr[i];
        }
        return res;
    }
    public void quickSort(int[] arr, int k, int leftBound, int rightBound) {
        if(leftBound >= rightBound) return;
        int mid = partition(arr, k, leftBound, rightBound);
        if(mid == k) return;
        else if(mid < k) quickSort(arr, k, mid + 1, rightBound);
        else quickSort(arr, k, leftBound, mid - 1);
    }
    public int partition(int[] arr, int k, int leftBound, int rightBound) {
        int pivot = arr[rightBound];
        int left = leftBound;
        int right = rightBound - 1;
        while(left <= right) {
            while(left <= right && arr[left] <= pivot) left++;
            while(left <= right && arr[right] > pivot) right--;
            if(left < right) swap(arr, left, right);
        }
        swap(arr, left, rightBound);
        return left;
    }
    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

————————————————————————————————————————

面试题41.数据流中的中位数

在这里插入图片描述
解题思路:

给定一个长度为 N 的无序数组,其中中位数的计算方法:首先对数组执行排序(使用 O(NlogN) 时间),然后返回中间元素即可(使用 O(1) 时间)。

根据以上思路,可以将数据流保存在一个列表中,并在添加元素时保持数组有序。此方法的时间复杂度为 O(N) ,其中包括: 查找元素插入位置 O(logN) (二分查找)、向数组某位置插入元素 O(N) (插入位置之后的元素都需要向后移动一位)。

建立一个 小顶堆 A 和 大顶堆 B,各保存列表的一半元素,且规定:

  • A 保存较大的一半,长度为 N / 2(N为偶数)或 (N + 1)/ 2(N为奇数)
  • B 保存较小的一半,长度为 N / 2(N为偶数)或 (N - 1)/ 2(N为奇数)
    在这里插入图片描述
    算法流程:设元素总数为 N = m + n,其中 m 和 n 分别为 A 和 B 中的元素个数。

addNum(num) 函数:

  • 1.当 m = n(即 N 为偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B,再将 B 堆顶元素插入至 A;
  • 2.当 m != n(即 N 为奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A,再将 A 堆顶元素插入至 B;

假设插入数字 num 遇到情况 1. 。由于 num 可能属于 “较小的一半” (即属于 B ),因此不能将 num 直接插入至 A 。而应先将 num 插入至 B ,再将 B 堆顶元素插入至 A 。这样做,如果num大于B的堆顶,则插入后num成为了B新的堆顶元素,此时就弹出插入A;如果num小于B的堆顶,此时弹出B的堆顶,再插入A,这样就可以始终保持 A 保存较大一半、 B 保存较小一半。

findMedian() 函数:

  • 1.当 m = n(即 N 为偶数):则中位数为(A 的堆顶元素 + B 的堆顶元素)/ 2
  • 2.当 m != n(即 N 为奇数): 则中位数为 A 的堆顶元素

当从数据流中读出的数的个数为偶数的时候,我们想办法让两个堆中的元素个数相等,两个堆顶元素的平均值就是所求的中位数;

当从数据流中读出的数的个数为奇数的时候,我们想办法让最小堆的元素个数永远比最大堆的元素个数多 1 个

class MedianFinder {
	Queue<Integer> minHeap, manHeap;
    /** initialize your data structure here. */
    public MedianFinder() {
		minHeap = new PriorityQueue<>(); //小顶堆,保存较大的一半
		maxHeap = new PriorityQueue<>((x, y) -> (y - x)); //大顶堆,保存较小的一半
    }
    
    public void addNum(int num) {
		if(minHeap.size() != maxHeap.size()) { 
			//因为约定小顶堆个数要多于大顶堆,而此时小顶堆元素已经比大顶堆多一个
			//所以要往大顶堆中加元素
			minHeap.add(num);
			maxHeap.add(minHeap.poll());
		} else {
			maxHeap.add(num);
			minHeap.add(maxHeap.poll());
		}
    }
    
    public double findMedian() {
		if(minHeap.size() != maxHeap.size()) return minHeap.peek();
		else return (minHeap.size() + maxHeap.size()) / 2;
    }
}
  • 时间复杂度:

    • 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间;
    • 添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。
  • 空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值