排序
- 035-数组中的逆序对(归并排序)
- 029-最小的K个数(堆排序)
- 029-最小的K个数(快速排序)
数组中的逆序对
利用「归并排序」计算逆序对,是非常经典的做法
最小的K个数
/*
在面试中,另一个常常问的问题就是这两种方法有何优劣.看
* 起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,
* 但是要注意到 快速选择算法的几点局限性:
*
* 第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了
*
* 第二,算法需要保存所有的数据。如果把数据看成输入流的话,
* 使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。
* 而快速选择的方法需要先保存下来所有的数据,再运行算法。
* 当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。
* 所以当数据量大的时候还是用基于堆的方法比较好。
*/
public class 最小的K个数 {
/**
* 由于使用了一个大小为 k 的堆,空间复杂度为 O(k);
* * 入堆和出堆操作的时间复杂度均为 O(logk)
* * 每个元素都需要进行一次入堆操作,故算法的时间复杂度为 O(nlogk)
* @param input
* @param k
* @return
*/
//尺寸为K的 大顶堆
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list=new ArrayList<>();
if(input==null||input.length==0||k==0||k>input.length) return list;
//构建大顶堆
Queue<Integer> heap=new PriorityQueue<>(k,(v1, v2)->Integer.compare(v2,v1));
for(int e:input){
if(heap.isEmpty()||heap.size()<k||e<heap.peek()){
heap.offer(e);
}
if(heap.size()>k){
heap.poll();//弹出最大的 堆顶元素
}
}
list.addAll(heap);
return list;
// return (ArrayList<Integer>) heap;错误
// for (int i = 0; i < k; i++) {
// list.addAll()
// }
}
/**
* 空间复杂度 O(1),不需要额外空间。递归 辅助栈
* 时间复杂度的分析方法和快速排序类似。
* 由于快速选择只需要递归一边的数组,时间复杂度小于快速排序,
* 期望时间复杂度为 O(n),最坏情况下的时间复杂度为 O(n^2)。
*
* 作者:nettee
* 链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/
* 来源:力扣(LeetCode)
* 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
* @param arr
* @param k
* @return
*/
public int[] getLeastNumbers(int[] arr, int k) {
// ArrayList
if(k==0) return new int[0];
if(arr==null||arr.length<=k) return arr;
select(arr,0,arr.length-1,k);
int[] res=new int[k];
// int[] res=Arrays.copyOf(arr,k);
for(int i=0;i<k;i++){
res[i]=arr[i];
}
return res;
}
public void select(int[] nums, int low, int high,int k){
int pivot=partiton(nums,low,high);
if(pivot==k) return;
else if(pivot>k){
select(nums,low,pivot-1,k);
}else{
//我们在右侧数组中寻找最小的 k-m 个数,
// 但是对于整个数组而言,这是最小的 k 个数。所以说,函数调用传入的参数应该为 k。
select(nums,pivot+1,high,k);
}
}
public int partiton(int[] nums, int low, int high) {
int key=nums[low];
int i=low,j=high+1;
while(true){
while((nums[++i]<key)&&i<high);
while((key<nums[--j])&&j>low);
//退出循环条件
if(i>=j) break;
swap(nums,i,j);
}
swap(nums,j,low);
return j;
}
public void swap(int[] nums,int i,int j){
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
}
位运算
- 011-二进制中1的个数
- 012-数值的整数次方
- 040-数组中只出现一次的数字
二进制中1的个数
public class Solution {
public int NumberOf1(int n) {
int sum=0;
while(n!=0){
if((n&1)!=0){
sum++;
}
n=n>>>1;//无符号右移,补0
}
return sum;
}
}
public class Solution {
public int hammingWeight(int n) {
int res = 0;
while(n != 0) {
res++;
n &= n - 1;
}
return res;
}
}
数值的整数次方
class Solution {
public double myPow(double x, int n) {
if(x==0) return 0;
double res=1.0;
long b=n;
if(b<0){
b=-b;
x=1/x;
}
while(b>0){
if((b&1)==1) res*=x;
x*=x;
b>>=1;
}
return res;
}
}
数组中只出现一次的数字||第一个只出现一次的字符
相反数互与 最低位1
class Solution {
public int[] singleNumbers(int[] nums) {
int num=0;
int a=0,b=0;
for(int n:nums){
num^=n;
}
//找num最低位1
int mask=num&(-num);
for(int n:nums){
if((n&mask)==0){
a^=n;
}else{
b^=n;
}
}
return new int[]{a,b};
}
}
class Solution {
public char firstUniqChar(String s) {
HashMap<Character,Integer> map=new HashMap<>();
char[] arr=s.toCharArray();
for(int i=0;i<arr.length;i++){
if(map.containsKey(arr[i])){
map.put(arr[i],map.get(arr[i])+1);
}
else{
map.put(arr[i],1);
}
}
// for(Map.Entry<Character,Integer> t:map.entrySet()){
// char m= (char) t.getKey();
// int n= (int) t.getValue();
// if(n==1) return m;
// }
for(char c:arr){
if(map.get(c)==1) return c;
}
return ' ';
}
}
搜索算法
001-二维数组查找
006-旋转数组的最小数字(二分查找)
037-数字在排序数组中出现的次数(二分查找)
二维数组查找
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix==null||matrix.length==0||matrix[0].length==0){
return false;
}
int rows=matrix.length,cols=matrix[0].length;
int i=0,j=cols-1;
while(i<rows&&j>=0){
if(matrix[i][j]==target){
return true;
}else if(matrix[i][j]<target){
i++;
}else{
j--;
}
}
return false;
}
}
旋转数组的最小数字
二分查找
class Solution {
public int minArray(int[] numbers) {
int i=0,j=numbers.length-1;
while(i<j){
int m=(i+j)/2;
if(numbers[m]>numbers[j]) i=m+1;
else if(numbers[m]<numbers[j]) j=m;
else j--;
}
return numbers[i];
}
}
数字在排序数组中出现的次数
排序数组中的搜索问题,首先想到 二分法 解决。
class Solution {
public int search(int[] nums, int target) {
// 搜索右边界 right
int i = 0, j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] <= target) i = m + 1;
else j = m - 1;
}
int right = i;
// 若数组中无 target ,则提前返回
if(j >= 0 && nums[j] != target) return 0;
// 搜索左边界 right
i = 0; j = nums.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(nums[m] < target) i = m + 1;
else j = m - 1;
}
int left = j;
return right - left - 1;
}
}
类似
34. 在排序数组中查找元素的第一个和最后一个位置
全排列
027-字符串的排列
字符串的排列
排列组合问题,一般是回溯法解
回溯法
class StringRank {
private ArrayList<String> res = new ArrayList<>();
private TreeSet<String> paths = new TreeSet<>();
private StringBuilder path = new StringBuilder();
private boolean[] visited;
public ArrayList<String> Permutation(String str) {
if (str == null || str.equals("")) {
return res;
}
char[] strs = str.toCharArray();
Arrays.sort(strs);
visited = new boolean[strs.length];
combination(strs, 0);
res.addAll(paths);
return res;
}
private void combination(char[] strs, int len) {
if (len == strs.length) {
paths.add(path.toString());
return;
}
for (int i = 0; i < strs.length; i++) {
if (!visited[i]) {
visited[i] = true;
path.append(strs[i]);
combination(strs, len + 1);
//Duang ~ 回溯 - 状态重置
visited[i] = false;
path.deleteCharAt(path.length() - 1);
}
}
}
}
会有问题:abc 虽然力扣通过了,但是剑指不通过
输出[abc, acb, bac, bca, cba, cab]
最后两个顺序有错误
class Solution {
char[] c;
List<String> list=new LinkedList<>();
public String[] permutation(String s) {
c=s.toCharArray();
dfs(0);
//添加排序
Collections.sort(list);
return list.toArray(new String[list.size()]);
}
public void dfs(int pos){
if(pos==c.length-1){
list.add(String.valueOf(c));
return;
}
HashSet<Character> set=new HashSet<>();
for(int i=pos;i<c.length;i++){
if(set.contains(c[i])) continue;
set.add(c[i]);
swap(i,pos);
//递归下一层
dfs(pos+1);
//回溯
swap(i,pos);
}
}
public void swap(int i,int j){
char tmp=c[i];
c[i]=c[j];
c[j]=tmp;
}
}
动态规划
030-连续子数组的最大和
052-正则表达式匹配
连续子数组的最大和
class Solution {
public int maxSubArray(int[] nums) {
//不改变原数组
int max=nums[0],cur=nums[0],pre=0;//pre为dp[-1]
for(int i:nums){
if(pre>=0) i+=pre;
pre=i;
max=Math.max(max,i);
}
return max;
// //动态规划
// //dp[i]以nums[i]为结尾的连续数组和
// //dp[i]=dp[i-1](>0)+nums[i]
// int max=nums[0];
// for(int i=1;i<nums.length;i++){
// nums[i]+=Math.max(nums[i-1],0);
// max=Math.max(max,nums[i]);
// }
// return max;
}
}
正则表达式匹配
以一个例子详解动态规划转移方程:
S = abbbbc
P = ab*d*c
1. 当 i, j 指向的字符均为字母(或 '.' 可以看成一个特殊的字母)时,
只需判断对应位置的字符即可,
若相等,只需判断 i,j 之前的字符串是否匹配即可,转化为子问题 f[i-1][j-1].
若不等,则当前的 i,j 肯定不能匹配,为 false.
f[i-1][j-1] i
| |
S [a b b b b][c]
P [a b * d *][c]
|
j
2. 如果当前 j 指向的字符为 '*',则不妨把类似 'a*', 'b*' 等的当成整体看待。
看下面的例子
i
|
S a b [b] b b c
P a [b *] d * c
|
j
注意到当 'b*' 匹配完 'b' 之后,它仍然可以继续发挥作用。
因此可以只把 i 前移一位,而不丢弃 'b*', 转化为子问题 f[i-1][j]:
i
| <--
S a [b] b b b c
P a [b *] d * c
|
j
另外,也可以选择让 'b*' 不再进行匹配,把 'b*' 丢弃。
转化为子问题 f[i][j-2]:
i
|
S a b [b] b b c
P [a] b * d * c
|
j <--
3. 冗余的状态转移不会影响答案,
因为当 j 指向 'b*' 中的 'b' 时, 这个状态对于答案是没有用的,
原因参见评论区 稳中求胜 的解释, 当 j 指向 '*' 时,
dp[i][j]只与dp[i][j-2]有关, 跳过了 dp[i][j-1].
class Solution {
public boolean isMatch(String s, String p) {
int m=s.length(),n=p.length();
boolean[][] dp=new boolean[m+1][n+1];
dp[0][0]=true;
for(int i=0;i<=m;i++){
for(int j=1;j<=n;j++){
if('*'==(p.charAt(j-1))){
dp[i][j]=dp[i][j-2];
if(matches(s,p,i,j-1)){
dp[i][j]=dp[i][j]||dp[i-1][j];
}
}else{
if(matches(s,p,i,j)){
dp[i][j]=dp[i-1][j-1];
}
}
}
}
return dp[m][n];
}
public boolean matches(String s,String p, int i,int j){
if(i==0){
return false;
}
if(p.charAt(j-1)=='.'){
return true;
}
return p.charAt(j-1)==s.charAt(i-1);
}
}
排序
035-数组中的逆序对(归并排序)
029-最小的K个数(堆排序)
029-最小的K个数(快速排序)
数组中的逆序对
暴力法会超时
归并排序法
方法栈+按DFS顺序访问
class Solution {
public int reversePairs(int[] nums) {
int len = nums.length;
if(len < 2) return 0;
int[] copy = new int[len];//避免修改原数组
for(int i = 0; i < len; i++) copy[i] = nums[i];
int[] tmp = new int[len];//辅助数组,用于归并两个有序数组
return reversePairs(copy, 0, len - 1, tmp); //方法重载机制 递归方法 左闭右闭区间
}
/**
* nums[left..right] 计算逆序对个数并且排序
*
* @param nums
* @param left
* @param right
* @param temp
* @return
*/
private int reversePairs(int[] nums, int left, int right, int[] tmp){
if(left == right) return 0; //递归终结者 只剩下一个元素 不含逆序对
int mid = left + (right - left) / 2;//此处采用此方式是因为(right + left) / 2中的right + left可能产生越界
int leftPairs = reversePairs(nums, left, mid, tmp);
int rightPairs = reversePairs(nums, mid + 1, right, tmp);
//优化步骤
//合并两个有序数组之前,如果能检测出 数组已经是有序的
//则不需要合并,直接将左右边的逆序对的个数
if(nums[mid] <= nums[mid + 1]) return leftPairs + rightPairs; // 此时,左边子数组的最大数小于右边子数组的最小数,直接合并即可,不会产生逆序对
int crossPairs = mergerAndCount(nums, left, mid, right, tmp); //crossPairs是将两个有序子数组归并为一个有序数组产生的逆序对
return crossPairs + rightPairs + leftPairs;
}
//nums[left..mid] 有序,nums[mid + 1..right] 有序
private int mergerAndCount(int[] nums, int left, int mid, int right, int[] tmp){
//全程采用一个数组tmp的原因有二:不必一直创建新的数组,节约资源;每次处理的都是子数组,如果创建新的数组会导致索引的处理很麻烦,容易出错
for(int i = left; i <= right; i++) tmp[i] = nums[i];
int i = left; //左边的有序数组的左边界
int j = mid + 1;//右边的有序数组的左边界
int count = 0;//计数器
for(int k = left; k <= right; k++){
if(i == mid + 1){//i到边界,只能将j归并回去
nums[k] = tmp[j]; //此时左边的子数组长度为0
j++;
}else if(j == right + 1){
nums[k] = tmp[i]; //此时右边的子数组长度为0
i++;
//i,j均在有效位置
}else if(tmp[i] <= tmp[j]){ //这儿必须得是“<=”,如是“<”,则归并排序是不稳定的
nums[k] = tmp[i]; //左边子数组的值较小,进入有序数组
i++;
}else{
nums[k] = tmp[j]; //此时,右边子数组的值较小,进入有序数组
j++;
count += (mid - i + 1);//左边子数组剩余的个数即为逆序对个数
}
}
return count;
}
}
import java.io.*;
public class Main {
public static int InversePairs(int [] array) {
if(array == null) return 0;
int[] tmp = new int[array.length];
return mergeSort(array, tmp, 0, array.length-1);
}
//归并排序,递归
private static int mergeSort(int[] array, int[] tmp, int low, int high) {
if(low >= high) return 0;
int res = 0, mid = low + (high - low) / 2;
res += mergeSort(array, tmp, low, mid);
res %= 1000000007;
res += mergeSort(array, tmp, mid + 1, high);
res %= 1000000007;
res += merge(array, tmp, low, mid, high);
res %= 1000000007;
return res;
}
//归并排序,合并
private static int merge(int[] array, int[] tmp, int low, int mid, int high) {
int i = low, i1 = low, i2 = mid + 1;
int res = 0;
while(i1 <= mid && i2 <= high) {
if(array[i1] > array[i2]) {
res += mid - i1 + 1;
res %= 1000000007;
tmp[i++] = array[i2++];
} else
tmp[i++] = array[i1++];
}
while(i1 <= mid)
tmp[i++] = array[i1++];
while(i2 <= high)
tmp[i++] = array[i2++];
for (i = low; i <= high; i++)
array[i] = tmp[i];
return res;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
str = str.substring(1, str.length()-1);
String[] valueArr = str.split(",");
int[] array = new int[valueArr.length];
for (int i = 0; i < valueArr.length; i++) {
array[i] = Integer.parseInt(valueArr[i]);
}
System.out.println(InversePairs(array));
}
}
327.区间和的个数
最小的K个数
import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Queue;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> list=new ArrayList<>();
if(input==null||input.length==0||k==0||k>input.length) return list;
//构建大顶堆
Queue<Integer> heap=new PriorityQueue<>(k,(v1, v2)->Integer.compare(v2,v1));
for(int e:input){
if(heap.isEmpty()||heap.size()<k||e<heap.peek()){
heap.offer(e);
}
if(heap.size()>k){
heap.poll();
}
}
list.addAll(heap);
return list;
//wrong return (ArrayList<Integer>) heap;
}
}
其他
002-替换空格
013-调整数组顺序使奇数位于偶数前面
028-数组中出现次数超过一半的数字
031-整数中1出现的次数(从1到n整数中1出现的次数)
032-把数组排成最小的数
033-丑数
041-和为S的连续正数序列(滑动窗口思想)
042-和为S的两个数字(双指针思想)
043-左旋转字符串(矩阵翻转)
046-孩子们的游戏-圆圈中最后剩下的数(约瑟夫环)
051-构建乘积数组
替换空格
public class Solution {
public String replaceSpace(StringBuffer str) {
int p1=str.length()-1;
for(int i=0;i<=p1;i++){
char c=str.charAt(i);
if(c==' ')
str.append(" ");
}
int p2=str.length()-1;
while(p2>p1&&p1>=0){
char c=str.charAt(p1--);
if(c==' '){
str.setCharAt(p2--,'0');
str.setCharAt(p2--,'2');
str.setCharAt(p2--,'%');
}else{
str.setCharAt(p2--,c);
}
}
return str.toString();
}
}
调整数组顺序使奇数位于偶数前面
第一反应用辅助list,但空间复杂度上去了
import java.util.*;
public class Solution {
public void reOrderArray(int [] array) {
if(array==null) return;
List<Integer> odd=new ArrayList<>();
List<Integer> even=new ArrayList<>();
int len=array.length;
for(int i=0;i<len;i++){
if(array[i]%2==0){
even.add(array[i]);
}else{
odd.add(array[i]);
}
}
//int[] res=new int[len];
for(int i=0;i<len;i++){
if(i<odd.size()){
array[i]=odd.get(i);
}else{
array[i]=even.get(i-odd.size());
}
}
}
}
数组中出现次数超过一半的数字
class Solution {
public int majorityElement(int[] nums) {
int x = 0, votes = 0, count = 0;
for(int num : nums){
if(votes == 0) x = num;
votes += num == x ? 1 : -1;
}
// 验证 x 是否为众数
for(int num : nums)
if(num == x) count++;
return count > nums.length / 2 ? x : 0; // 当无众数时返回 0
}
}
作者:jyd
链接:https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/solution/mian-shi-ti-39-shu-zu-zhong-chu-xian-ci-shu-chao-3/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1~n 整数中 1 出现的次数
递归
丑数
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index==0) return 0;
int[] dp=new int[index];
dp[0]=1;
int p2=0,p3=0,p5=0;
for(int i=1;i<index;i++){
dp[i]=Math.min(Math.min(dp[p2]*2,dp[p3]*3),dp[p5]*5);
if(dp[i]==dp[p2]*2){
p2++;
}
if(dp[i]==dp[p3]*3){
p3++;
}
if(dp[i]==dp[p5]*5){
p5++;
}
}
return dp[index-1];
}
}
扑克牌顺子
import java.util.HashSet;
public class Solution {
public boolean isContinuous(int [] numbers) {
if(numbers==null||numbers.length==0) return false;
int max=0,min=14;
HashSet<Integer> set=new HashSet<>();
for(int i=0;i<numbers.length;i++){
if(numbers[i] == 0) continue; // 跳过大小王
min=Math.min(min,numbers[i]);
max=Math.max(max,numbers[i]);
if(set.contains(numbers[i]))
{
return false;
}
set.add(numbers[i]);
}
return max-min<5;
}
}