算法基础
数据结构与算法
字符串匹配
KMP算法:
字符串算法之KMP(字符串匹配)kmp字符串匹配算法青萍之末的博客-CSDN博客
各大排序算法:
冒泡排序:
选择排序:
类似于冒泡算法,不断找到乱序数组中最小值的下标,将其余首元素交换,最终全部变为有序。
void Swap(int *arr, int a, int b)
{
int tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
void SimpleSelectSort(int *arr,int len)
{
int min;
for (int i = 0;i < len - 1;i++)
{
min = i;
for (int j = i + 1;j < len;j++)
{
if (arr[min] > arr[j])
{
min = j;
}
}
if (min != i)
{
Swap(arr,min,i);
}
}
}
插入排序:
相当于摸牌的排序方法
维护一个有序数组,每次冲无序数组中取一个数字,依次与有序数组从大到小比较,插入到合适的位置,最终得到有序数组。但实际上,并不需要两个数组,数组中前半部分为有序,后半部分为无序。
public void doInsertSort(int[] array){
for(int index = 1; index<array.lengh; index++){//外层向右的index,即作为比较对象的数据的index
int temp = array[index];//用作比较的数据
int leftindex = index-1;
while(leftindex>=0 && array[leftindex]>temp){//当比到最左边或者遇到比temp小的数据时,结束循环
array[leftindex+1] = array[leftindex];
leftindex--;
}
array[leftindex+1] = temp;//把temp放到空位上
}
}
快速排序:
类比于小兵排队,每次从数组中取第一个数作为key,设置开头和末尾为标志位low和high,在while循环中,先比较high位数字与标志位,找到比标志位小的数字,low与high交换,此时key位于high位,在将low从低到高与key比较,直到找到比key大的位置,再将low与high比较,此时key位于low位,在不断的交换中,最终low==high,此时key也位于数组的中间位,大数组被中间值分为两个小数组,最终小数组的长度为1,数组变为有序数组。
public void quickSort(int[] coins){
qSort(coins, 0, coins.length -1);
}
public void qSort(int[] L, int low, int high){
int p;
if (low < high){
p = partion(L, low,high);
qSort(L, low, p-1);
qSort(L, p+1, high);
}
return;
}
public int partion(int[] L, int low,int high){
int key;
key = L[low];
while (low < high){
while (low < high && L[high]>=key)
high --;
swap(L, low, high);
while (low < high && L[low] <= key)
low ++;
swap(L, low, high);
}
return low;
}
public void swap(int[] L, int a, int b){
int temp = L[a];
L[a] = L[b];
L[b] = temp;
return;
}
void quick(vector<int>& array, int begin, int end){
int p;
if (begin<end){
p = partion(array,begin,end);
quick(array,begin,p-1);
quick(array,p+1,end);
}
return;
}
void swap(vector<int>& array,int a, int b){
int temp = array[a];
array[a] = array[b];
array[b] = temp;
return;
}
int partion(vector<int>& array,int begin, int end){
int key = array[begin];
while (begin<end){
while (key<=array[end] && begin<end){
end--;
}
swap(array,begin,end);
while(array[begin]<=key && begin<end){
begin++;
}
swap(array,begin,end);
}
return begin;
}
shell排序
主要思想就是不断缩小gap,每次根据gap将数字分组,插入排序保证每组有序。当gap缩小到1时,数字有序。
public static void sort(Comparable[] arr) {
int j;
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
Comparable tmp = arr[i];
for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = tmp;
}
}
}
堆排序
归并排序
(1)划分:将待排序序列划分为两个长度相等的子序列
(2)求解子问题:分别对这两个子序列进行排序,得到两个有序子序列;
(3)合并:将这两个有序子序列合并成一个有序序列。
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡 | O(n^2) | O(1) | true |
选择 | O(n^2) | O(1) | false |
插入 | O(n^2) | O(1) | true |
快速排序 | O(n*lgn) | O(lgn) | false |
三数之和
排序+双指针
算法流程:
- 特判,对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []。
- 对数组进行排序。
- i[0,n-3]遍历排序后数组:
- 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。
- 对于重复元素:跳过,避免出现重复解
- 令左指针 L=i+1,右指针 R=n−1,当 L<R 时,执行循环:
- 当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R 移到下一位置,寻找新的解
- 若和大于 0,说明 nums[R] 太大,R左移(跳过值相同的元素,用while)
- 若和小于 0,说明 nums[L] 太小,L右移(跳过值相同的元素)
链表队列
两两交换链表中的结点
队列的最大值
请定义一个队列并实现函数 max_value
得到队列里的最大值,要求函数max_value
、push_back
和 pop_front
的均摊时间复杂度都是O(1)。
若队列为空pop_front
和 max_value
需要返回 -1
当然我们可以直接使用Queue来作为队列,力扣官方使用一个Deque来计算max_value,代码如下:
class MaxQueue {
Queue<Integer> q;
Deque<Integer> d;
public MaxQueue() {
q = new LinkedList<Integer>();
d = new LinkedList<Integer>();
}
public int max_value() {
if (d.isEmpty()) {
return -1;
}
return d.peekFirst();
}
public void push_back(int value) {
while (!d.isEmpty() && d.peekLast() < value) {
d.pollLast();
}//这里直接保证Deque是递减的,且后来的大数会将先来的小数踢出Deque
d.offerLast(value);
q.offer(value);
}
public int pop_front() {
if (q.isEmpty()) {
return -1;
}
int ans = q.poll();
if (ans == d.peekFirst()) {
d.pollFirst();
}
return ans;
}
}
注意在push_back函数中,从队列尾部插入元素时,我们可以提前取出Deque队列中所有比这个元素小的元素,使得Deque队列中只保留对结果有影响的数字。这样的方法等价于要求维持Deque队列单调递减,即要保证每个元素的前面都没有比它小的元素。
之前一直想不通,这样做Deuqe就和queue不一样长了,但仔细一想,对于先进入队列的数据,先进先出,小的数被pop出去的时候,此时的max_value肯定不会是小数的,所以,可以让后来的大数直接把先来的小数踢出Deque。
红黑树
红黑树与平衡二叉树:
平衡二叉树要求左右子树的高度差不超过1,相对于普通二叉树在进行插入和删除操作时需要大量的旋转操作,增加成本,而红黑树则是通过牺牲掉部分平衡性,即保证了二叉树的平衡(根到叶子结点的长度,最长的不会超过最短的两倍),使查找成本不会退化为线性表,也降低了旋转的操作。
红黑树的特征:
- 节点不是黑色,就是红色(非黑即红)
- 根节点为黑色
- 叶节点为黑色(叶节点是指末梢的空节点
Nil
或Null
) - 一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
- 每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
红黑树结点定义
class RedBlackTreeNode {
public int val;
public RedBlackTreeNode left;
public RedBlackTreeNode right;
// 记录节点颜色的color属性,暂定true表示红色
public boolean color;
// 为了方便迭代插入,所需的parent属性
public RedBlackTreeNode parent;
// 一些构造函数,根据实际需求构建
public RedBlackTreeNode() {
}
}
红黑树定义
public class RedBlackTree {
// 当前红黑树的根节点,默认为null
private RedBlackTreeNode root;
}
二叉树的遍历(前中后序,(非)递归)
根据头结点在 左子树和右子树的三种插入分为前中后三中顺序遍历。
前序遍历
//利用栈前序遍历 STL stack
void Show_Pre_OrderII(BinaryTree root){
stack<BinaryTree> s;
BinaryTree t = root;
while(t || !s.empty()){
while(t){
printf("%c ", t->data);
s.push(t);
t = t->leftchild; //一直往左边走
}
t = s.top();
s.pop();
t = t->rightchild;
}
}
中序遍历
同样使用栈来模拟递归的过程,先定义一个遍历指针 t 指向 root,栈用来存储当前遍历到的节点。大致过程就是先一直往左走,同时将当前节点入栈,一直遍历到最左边的节点为止;然后取出栈顶节点,打印节点数据,访问其右子树,再重复上述操作。可以看出迭代版中序遍历和前序遍历的思路只在打印数据的地方有一点差别。
大致操作如下:(1)一直向左遍历,将节点入栈,直到到达最左边;(2)取栈顶节点,打印节点数据;(2)前往右子树,重复上述操作。
整个迭代的过程和前序遍历大致一致,只有打印数据位置不同,停止迭代的条件和前序遍历一样,也是 t == NULL 并且 stack为空。
//利用栈中序遍历 STL stack
void Show_Infix_OrderII(BinaryTree root){
stack<BinaryTree> s;
BinaryTree t = root;
while(t || !s.empty()){
while(t){
s.push(t);
t = t->leftchild; //一直往左边走
}
t = s.top();
s.pop();
printf("%c ", t->data);
t = t->rightchild;
}
}
后序遍历
后序遍历的迭代写法比前面的两种要稍微复杂一点,同样使用栈来模拟递归的过程,先定义一个遍历指针 t 指向 root,栈用来存储当前遍历到的节点。同时还需要定义一个指针 last 标记上一次访问过的节点。我们依旧需要先一直往左走,并且将走过的节点入栈,直到到达最左边。然后取出当前栈顶节点,讨论是否访问当前出栈节点。后序遍历的顺序是左子树–>右子树–>根节点,其实讨论的就是当前出栈的这个子树根节点是否能够被访问。当当前出栈节点的右子树为NULL时,则当前出栈节点可以访问,那么仅此一种情况吗?并不是,假如某个出栈子树的根节点,存在右子树,但是这个右子树已经遍历完毕了,那么也没有再访问右子树的需要,此时同样可以访问出栈节点。满足这个条件只会在左子树已经访问过并且右子树根节点为上一个访问过的节点情况下出现,所以我们只需要用一个 last 来记录当前访问过的节点就行了,为了防止出现死循环,还需要将 t 置NULL。如果这两个条件都不满足,那么当前出栈节点不能访问,需要重新入栈,并且转而访问右子树。
大致操作如下:(1)一直向左遍历,将节点入栈,直到到达最左边;(2)取栈顶节点,讨论是否可以访问当前出栈节点;(3)如果 t == NULL 或者 t == last,则可以访问,并用 last 记录当前访问的节点,将 t 置NULL;否则不能访问,将当前出栈节点重新入栈,转而前往右子树。
停止迭代的条件和前序遍历、中序遍历一样,也是 t == NULL 并且 stack为空。
//利用栈后序遍历 STL stack
void Show_Post_OrderII(BinaryTree root){
stack<BinaryTree> s;
BinaryTree t = root, last;
while(t || !s.empty()){
while(t){
s.push(t);
t = t->leftchild; //先到达最左侧节点
}
t = s.top();
s.pop();
//如果当前节点无右子树或者右子树根节点为上一个访问过的节点
if(!t->rightchild || t->rightchild == last){
printf("%c ", t->data);
last = t; //记录当前访问过的节点
t = NULL;
}else{
s.push(t); //否则将当前节点重新入栈
t = t->rightchild; //转而前往其右子树
}
}
}
大根堆
实际上可以直接使用STL库中的优先级队列
priority_queue<int, vector<int>, less<int>>s;//less表示按照递减(从大到小)的顺序插入元素
priority_queue<int, vector<int>, greater<int>>s;//greater表示按照递增(从小到大)的顺序插入元素
使用c++ 数组实现大根堆:
void swap(int arr[], int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
void bigRootHeap(int arr[], int size){
for(int i = 0; i < size; i++){
for(int j = i; j >= 0; j = (j-1)/2){
if(arr[j] > arr[(j-1)/2]){
swap(arr, j, (j-1)/2);
}else{
break;
}
}
}
}
arr[0]为最大值,若想依次弹出大值,则:
for (int i = 0; i < k; ++i) {
cout<<arr[0]<<" ";
swap(arr, 0, arr.size-k-1);
bigRootHeap(arr, arr.size-k);
} // 每次都要重新构建大根堆
字符串哈希+前缀和
前缀和的重要思想
使用前缀和数组中,两个元素的运算值来表示一个区间的值,当我们在计算前缀和数组时,只是会遍历一次,计算量为O(n),而后每次寻找区间的值的计算量都为O(1),相比于直接在原数组中计算区间,其计算量为区间的长度O(m)
则两者计算量的差距体现为:?
重复的DNA序列
给定一个表示 DNA序列 的字符串 s
,返回所有在 DNA 分子中出现不止一次的 长度为 10
的序列(子字符串)。
双指针
盛最多水的容器
有如下规则:
- 若向内移动短板 ,水槽的短板 min(h[i],h[j])可能变大,因此下个水槽的面积 可能增大 。
- 若向内移动长板 ,水槽的短板 min(h[i],h[j])不变或变小,因此下个水槽的面积 一定变小 。
算法流程:初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。
递归
什么问题可以用递归解决?
- 主问题可以拆分为多个子问题;
- 主问题和子问题的解题思路一致;
- 存在终止条件。
递归问题如何解决?
- 找到递推公式;
- 找到终止条件;
- 将他们翻译成代码
回溯法
使用DFS遍历搜索树,找到所有可行解
回溯三部曲
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
回溯法的核心思想也是划分状态,找到搜索树,有些题目搜索树的高度是流程,有些则较为抽象,其中较难题的当属警卫问题。
世界名画陈列馆
(137条消息) 世界名画陈列馆(最少机器人问题和不重复监视问题)_中都的博客-CSDN博客
警卫问题
全排列的深度优先搜索树
void dfs(int step) //step表示站在第几个盒子前
{
for(i=1; i<=n; i++)
{
if(!book[i])
{
a[step] = i;
book[i] = 1;
dfs(step+1);
book[i] = 0;
}
}
}
将n个数字依次放在n个箱子中,对应的搜索树是n颗高为n的多叉树,其中每一层的子树都在递减1。
动态规划
确定状态,找到状态转移方程
经典合集:常见的动态规划问题分析与求解 - 五岳 - 博客园 (cnblogs.com)
带权重问任务调度题
我们在考虑这个问题时,注意的是要留0,即p[0]=0 和 dp[0]=0 ,而对于每项任务,是从1开始的,这样的话可以有p[1] = 0,同样 dp[1] = dp[0] + w[1],为了保持i统一,我们记录任务数据的数组也从1开始即可。
struct task{
int begintime;
int endtime;
int value;
};
bool cmp(task a,task b){
return a.endtime < b.endtime; // 以结束时间比较两个任务,从大到小
}
class DP{
private:
task* taskSeq;
int* dp;
int* p;
public:
int seqNum;
DP(int seqNum){
this->seqNum = seqNum;
}
void init(){
taskSeq = new task[this->seqNum+1];
dp = new int[this->seqNum+1];
p = new int[this->seqNum+1];
for (int i = 1; i < seqNum+1; i++) {
cin>>taskSeq[i].begintime>>taskSeq[i].endtime>>taskSeq[i].value; //输入任务序列
}
for (int i = 1; i < seqNum+1; i++) {
cout<<taskSeq[i].begintime<<" "<<taskSeq[i].endtime<<" "<<taskSeq[i].value<<endl;
}
}
void solute(){
sort(taskSeq+1,taskSeq+seqNum+1, cmp);
for (int i = 1; i < seqNum+1; i++) {
cout<<taskSeq[i].endtime<<" ";
}
cout<<endl;
memset(p,0,sizeof(p));
for (int i = 1; i < this->seqNum+1; i++) {
for (int j = i-1; j >0 ; j--) {
if (taskSeq[j].endtime <= taskSeq[i].begintime){
p[i]=j;//不影响i任务的最后一个任务i
break;
}
}
}
for (int i = 1; i < seqNum+1; i++) {
cout<<p[i]<<" ";
}
cout<<endl;
memset(dp,0,sizeof(dp));
dp[0] = 0;
for (int i = 1; i < seqNum; i++) {
dp[i] = max(dp[i-1],dp[p[i]]+taskSeq[i].value);
}
for (int i = 1; i < seqNum+1; i++) {
cout<<dp[i]<<" ";
}
cout<<endl;
cout<<dp[seqNum-1];
}
};
多段图最短路径
动态规划法解多段图最短路径问题 - 乌漆WhiteMoon - 博客园 (cnblogs.com)
使用pd[n+1]记录从1结点到i结点的最短路径
使用path[n+1]记录每个节点的前驱节点
灌溉花园最小水龙头
在 x 轴上有一个一维的花园。花园长度为 n
,从点 0
开始,到点 n
结束。
花园里总共有 n + 1
个水龙头,分别位于 [0, 1, ..., n]
。
给你一个整数 n
和一个长度为 n + 1
的整数数组 ranges
,其中 ranges[i]
(下标从 0 开始)表示:如果打开点 i
处的水龙头,可以灌溉的区域为 [i - ranges[i], i + ranges[i]]
。
请你返回可以灌溉整个花园的 最少水龙头数目 。如果花园始终存在无法灌溉到的地方,请你返回 -1 。
建立状态 int[] dp,dp[i]表示覆盖[0,i)需要最少的水龙头s
美团:车辆调度
当建立高维状态时,考虑是否可以类比01背包问题,即i状态只取决于i-1状态,从而减1维。
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int a = input.nextInt();
int b = input.nextInt();
int[][] dp = new int[a + 1][b + 1];
int[][] profits = new int[n + 1][2];
for (int i = 1; i <= n; i++) {
profits[i][0] = input.nextInt();
profits[i][1] = input.nextInt();
}
for (int i = 1; i <= n; i++) { //派出i辆车
for (int j = a; j >= 0; j--) { //向A地派出j辆
for (int k = b; k >= 0; k--) { //向B地派出k辆
if (j == 0 && k == 0) continue;
if (k == 0) {
//将第i辆车不派出或者派到A地
dp[j][k] = Math.max(dp[j][k], dp[j - 1][k] + profits[i][0]);
}
else if (j == 0) {
//将第i辆车不派出或者派到B地
dp[j][k] = Math.max(dp[j][k], dp[j][k - 1] + profits[i][1]);
}
else {
//将第i辆车不派出或者派到A地或者派到B地
dp[j][k] = Math.max(dp[j][k],
Math.max(dp[j - 1][k] + profits[i][0], dp[j][k - 1] + profits[i][1]));
}
}
}
}
int res = dp[a][b];
System.out.println(res);
}
}
【算法】小团的车辆调度(美团2021校招题)_Kant101的博客-CSDN博客
多边形三角剖分最低得分
1039. 多边形三角剖分的最低得分 - 力扣(Leetcode)
正则表达式匹配
是我永远的痛
收集巧克力
动态规划:使用dp[i][j]表示在总执行i次操作,第j种类型巧克力的最小代价,这里是不计算操作成本的。
dp[i][j] = min( dp[i-1][j], nums[(j-i+n)%n] ) 即使说,第j种类型在第i次操作前就已经有了
得到所有dp后,遍历每个操作次数(行),所有类型代价求和再加上操作成本,最小值即为取指。
这里为了防止溢出我使用了java里的Math.addExact(a,b) 和Math.multiplyExact(a,b)这里要注意,a和b要一样的类型哦,math会判断是否溢出,并抛出ArithmeticException异常
后来发现,这道题使用long数据类型是不会溢出的,只是我在对x(操作成本)相乘时,x是int型。乘积结果返回的是int,所以溢出了。
剪绳子
由于大数可以有两个小数相加求和,所以想到使用动态规划求解
dp[i] 表示i被拆分后的最大乘积,由于0和1不能拆分所以dp[0] = dp[1] = 0
对于大于1点整数k,其可以被拆分为两个正整数i和k-i(0<i<k),由于i从1遍历到了k-1,这里我们都不拆分i,而对于k-i是可以拆分的,只有当其为1时不可继续拆分,这里就直接为i*(k-i),若其可拆分,则为i*dp[k-i],对这两个结果取最大值。
大数越界问题
要求对最大值取余,对于剪绳子问题,如果n特别大,那么在动态规划过程中会有很多的数据会越界,如果对每个结果都使用了取余,那么势必会改变最终的结果不是最优解,因为MOD
和 MAX
不满足交换律,所以,目前不能使用动态规划了,只能使用贪心算法,这个贪心规则真的很难发现啊:
最优解就是尽可能地分解出长度为 3
的小段。 但是我们要防止长度为 1
的小段出现(当剩余长度 <= 4
时,便不再分割)
class Solution {
public:
int cuttingRope(int n) {
if (n == 2) return 1;
if (n == 3) return 2;
if (n == 4) return 4;
long long res = 1;
while (n > 4) {
n -= 3;
res = res * 3 % 1000000007;
}
res = res * n % 1000000007;
return (int) res;
}
};
自动机
聪明的编辑
(152条消息) 字节跳动—万万没想到之聪明的编辑_wolf鬼刀的博客-CSDN博客
表示数值的字符串
使用有限自动状态机判断字符串是否为一个数值:
有限自动状态机为:
在java中使用enum枚举类表示状态,用map表示状态转移函数
class Solution {
public boolean isNumber(String s) {
Map<State, Map<CharType, State>> transfer = new HashMap<State, Map<CharType, State>>();
Map<CharType, State> initialMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_SPACE, State.STATE_INITIAL);
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
put(CharType.CHAR_SIGN, State.STATE_INT_SIGN);
}};
transfer.put(State.STATE_INITIAL, initialMap);
Map<CharType, State> intSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
}};
transfer.put(State.STATE_INT_SIGN, intSignMap);
Map<CharType, State> integerMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_POINT, State.STATE_POINT);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_INTEGER, integerMap);
Map<CharType, State> pointMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_POINT, pointMap);
Map<CharType, State> pointWithoutIntMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
}};
transfer.put(State.STATE_POINT_WITHOUT_INT, pointWithoutIntMap);
Map<CharType, State> fractionMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_FRACTION, fractionMap);
Map<CharType, State> expMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SIGN, State.STATE_EXP_SIGN);
}};
transfer.put(State.STATE_EXP, expMap);
Map<CharType, State> expSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
}};
transfer.put(State.STATE_EXP_SIGN, expSignMap);
Map<CharType, State> expNumberMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_EXP_NUMBER, expNumberMap);
Map<CharType, State> endMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_SPACE, State.STATE_END);
}};
transfer.put(State.STATE_END, endMap);
int length = s.length();
State state = State.STATE_INITIAL;
for (int i = 0; i < length; i++) {
CharType type = toCharType(s.charAt(i));
if (!transfer.get(state).containsKey(type)) {
return false;
} else {
state = transfer.get(state).get(type);
}
}
return state == State.STATE_INTEGER || state == State.STATE_POINT || state == State.STATE_FRACTION || state == State.STATE_EXP_NUMBER || state == State.STATE_END;
}
public CharType toCharType(char ch) {
if (ch >= '0' && ch <= '9') {
return CharType.CHAR_NUMBER;
} else if (ch == 'e' || ch == 'E') {
return CharType.CHAR_EXP;
} else if (ch == '.') {
return CharType.CHAR_POINT;
} else if (ch == '+' || ch == '-') {
return CharType.CHAR_SIGN;
} else if (ch == ' ') {
return CharType.CHAR_SPACE;
} else {
return CharType.CHAR_ILLEGAL;
}
}
enum State {
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END
}
enum CharType {
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_SPACE,
CHAR_ILLEGAL
}
}
并查集
Node数据结构,包含父结点和总和
class Node {N //并查集结点的数据结构
public int parent; //结点所属集合树的树根
public int sum; //结点所属集合的和
Node(int parent, int sum) {
this.parent = parent;
this.sum = sum;
}
}
//找到集合的父亲
public int find(Node root[],int i){
while(root[i].parent != -1){
i = root[i].parent;
}
return i;
}
//将两个节点合并并且相加
public int join(Node root[],int i,int j){
int parent_i = find(root,i);
int parent_j = find(root,j);
int sum = root[parent_i].sum + root[parent_j].sum;
if(parent_i != parent_j){
root[parent_j].parent = parent_i;
root[parent_i].sum = sum;
}
return sum;
}
滑动窗口
参考 无重复字符的最长子串
public int lengthOfLongestSubstring(String s){
if (s.length()==0)
return 0;
HashMap<Character,Integer> record = new HashMap<>();
int left=-1;
int right=0;
int res = 1;
while (right < s.length()){
if (record.containsKey(s.charAt(right))){
left = Math.max(record.get(s.charAt(right)),left);//窗口改变
}
res = Math.max(res,right-left);
record.put(s.charAt(right),right);
right++;
}
return res;
}
滑动窗口可以解决很多序列累积和累和的问题,例如:
和为s的连续正数序列
输出所有和为target的连续正整数序列(至少含有两个数)
public int[][] findContinuousSequence(int target) {
int i=1,j=2;
int sum = 3;
ArrayList<int[]> res = new ArrayList<>();
while (i<j){
if (sum<target){
j = j+1;
sum += j;
}
if (sum==target){
int[] a = new int[j-i+1];
for (int k=i;k<=j;k++){
a[k-i] = k;
}
res.add(a);
j = j+1;
sum += j;
}
if (sum>target){
sum -= i;
i = i+1;
}
}
int[][] r = new int[res.size()][];
for (int x=0;x<res.size();x++){
r[x] = res.get(x);
}
return r;
}
优先级队列
PriorityQueue
使用匿名类和lambda表达式来重载PriorityQueue
的交换器
PriorityQueue<Integer[]> p = new PriorityQueue<Integer[]>((a,b)->{
long n = (long)(a[1]+1)*a[1]*(b[1]-b[0]);
long m = (long)(b[1]+1)*b[1]*(a[1]-a[0]);
if (n<m)
return -1;
else if (n>m) {
return 1;
}else return 0;
});
//return 1则交换
注意:java float不是基本类型,比较需要使用compareTo (float)
关于质数
裴蜀定理
最大公因数
辗转相除法:
int gcd(int t1, int t2) {
return t2 == 0 ? t1 : gcd(t2, t1 % t2);
}
//
int gcd(int num1, int num2) {
while (num2 != 0) {
int temp = num1;
num1 = num2;
num2 = temp % num2;
}
return num1;
}
最小公倍数
long long lcm(long long t1, long long t2) {
return t1 * t2 / gcd(t1, t2);
}
素数判定
素数判定算法_Frost_Bite的博客-CSDN博客_素数检测算法
匈牙利算法
二分图
(137条消息) 匈牙利算法java版本_一惞的博客-CSDN博客
算法学习笔记(5):匈牙利算法 - 知乎 (zhihu.com)
int M, N; //M, N分别表示左、右侧集合的元素数量
int Map[MAXM][MAXN]; //邻接矩阵存图
int p[MAXN]; //记录当前右侧元素所对应的左侧元素
bool vis[MAXN]; //记录右侧元素是否已被访问过
bool match(int i)
{
for (int j = 1; j <= N; ++j)
if (Map[i][j] && !vis[j]) //有边且未访问
{
vis[j] = true; //记录状态为访问过
if (p[j] == 0 || match(p[j])) //如果暂无匹配,或者原来匹配的左侧元素可以找到新的匹配
{
p[j] = i; //当前左侧元素成为当前右侧元素的新匹配
return true; //返回匹配成功
}
}
return false; //循环结束,仍未找到匹配,返回匹配失败
}
int Hungarian()
{
int cnt = 0;
for (int i = 1; i <= M; ++i)
{
memset(vis, 0, sizeof(vis)); //重置vis数组
if (match(i))
cnt++;
}
return cnt;
}
贪心
基本思路
- 建立数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的解局部最优解合成原来解问题的一个解。
(145条消息) 算法基础(Java)–贪心算法_米奇罗的博客-CSDN博客
两个栈模仿队列
小的栈用作存储栈,大的栈用作缓冲栈。
用俩个栈模拟实现一个队列,如果栈的容量分别是O和P(O>P),那么模拟实现的队列最大容量是多少
答:2p+1(p是较小的容量)
首先向长栈中push p个,再将p个pop到短栈,此时的短栈中是p个有序的数字,且从短栈中pop是按照先后顺序的。这时在向空的长栈push p+1个,达到最大容量
记忆化搜索
N皇后问题
「leetcode」51. N皇后【回溯算法】详细图解! - 知乎 (zhihu.com)
分枝定界算法
为每个结点确定边界值,一个不合法的最小(大)值,一个合法的最大(小)值(一般用贪心),对搜索树进行剪枝,每个结点有一个可能值lb,计算公式变化,超出边界则剪枝,对于叶子结点,其lb最小(大)为最优解。
例如STP问题:
下界为每一行最短两条边的和除以2为14(每个城市都有两条邻接边,一条是进入这个城市的,另一条是离开这个城市的,那么,如果把矩阵中每一行最小的两个元素相加再除以2,如果图中所有的代价都是整数,再对这个结果向上取整,就得到了一个合理的下界)
伪代码:
分冶算法
分而治之
二分法、快速排序就算是一种分冶法
最大子段和
#include<iostream>
using namespace std;
int MaxSum(int a[ ], int left, int right)
{
int sum=0;
int center,leftsum,rightsum,s1,lefts,i,s2,rights,j;
if (left==right) //如果序列长度为1,直接求解
{
if (a[left]>0)
sum=a[left];
else
sum=0;
}
else
{
center=(left+right)/2; //划分
leftsum=MaxSum(a, left, center);//对应情况①,递归求解
rightsum=MaxSum(a, center+1, right);//对应情况②,递归求解
s1=0;
lefts=0;
//以下对应情况③,先求解s1
for (i=center; i>=left; i--)
{
lefts+=a[i];
if (lefts>s1)
s1=lefts;
}
s2=0;
rights=0; //再求解s2
for (j=center+1; j<=right; j++)
{
rights+=a[j];
if (rights>s2)
s2=rights;
}
sum=s1+s2; //计算情况③的最大子段和
if (sum<leftsum)
sum=leftsum;
//合并,在sum、leftsum和rightsum中取较大者
if (sum<rightsum)
sum=rightsum;
}
return sum;
}
int main()
{
int n;
int a[100];
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
cout<<MaxSum(a,0,n-1);
}
摆动排序
给你一个整数数组 nums
,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]...
的顺序。
寻找中位数
快速选择法:寻找第k大的数字(O(n)):
是快速排序算法的优化,在quick()返回基准数的位置后,判断中位数在哨兵的左边还是右边,只继续对一边进行递归,直到找到第K个数。
在C++中,可以用STL的nth_element()函数进行快速选择,这一函数的效果是将数组中第n小的元素放在数组的第n个位置,同时保证其左侧元素不大于自身,右侧元素不小于自身。
重建二叉树
根据前序遍历数组和中序遍历数组还原二叉树,算法很巧妙:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] preorder;
public int[] inorder;
public HashMap<Integer,Integer> preMap = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length ==0)
return null;
this.preorder = preorder;
this.inorder = inorder;
for (int i=0;i<preorder.length;i++){
preMap.put(preorder[i],i);
}
return frac(0,inorder.length-1);
}
public TreeNode frac(int begin,int end){
if (begin == end){
TreeNode t = new TreeNode(inorder[begin]);
return t;
}
int preMin = preMap.get(inorder[begin]);
int index = begin;
for (int i=begin+1;i<=end;i++){
if (preMap.get(inorder[i])<preMin){
preMin = preMap.get(inorder[i]);
index = i;
}
}
TreeNode t = new TreeNode(inorder[index]);
if (index != begin)
t.left = frac(begin,index-1);
if (index != end)
t.right = frac(index+1,end);
return t;
}
}
快速幂解法(Pow)
即实现pow(x, n),注意n可能为负,这里使用分冶法,则只需要计算logN次乘积
public double myPow(double x, int n) {
if (n == 0)
return 1.0;
return n > 0 ? frac(x, n) : (1.0 / frac(x, -n));
}
public double frac(double x, int n) {
if (n == 0)
return 1.0;
int half = n / 2;
double sub = frac(x, half);
return n % 2 == 0 ? sub * sub : sub * sub * x;
}
图
判断有环:
- DFS:使用DFS构造深度优先搜索树,每次迭代记录当前节点的父节点,和已访问的结点,遍历所有邻接结点,若存在已访问结点且不是父节点(入环点),直接返回true(存在环),此方法,可以使用数组记录迭代中访问的父节点,入环点以下则为环。
- 并查集:若依次访问每条边,若边的两端属于不同集合,则合并,若属于相同集合,说明存在环,此方法无法输出环。
这里引用一道选课的题目:
你这个学期必须选修
numCourses
门课程,记为0
到numCourses - 1
。在选修某些课程之前需要一些先修课程。 先修课程按数组
prerequisites
给出,其中prerequisites[i] = [ai, bi]
,表示如果要学习课程ai
则 必须 先学习课程bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。请你判断是否可能完成所有课程的学习?如果可以,返回
true
;否则,返回false
。
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adjacency = new ArrayList<>();
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
int[] flags = new int[numCourses];
for(int[] cp : prerequisites)
adjacency.get(cp[1]).add(cp[0]);
for(int i = 0; i < numCourses; i++)
if(!dfs(adjacency, flags, i)) return false;
return true;
}
private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
if(flags[i] == 1) return false;
if(flags[i] == -1) return true;
flags[i] = 1;
for(Integer j : adjacency.get(i))
if(!dfs(adjacency, flags, j)) return false;
flags[i] = -1;
return true;
}
依次对每个结点进行深度遍历,在递归的过程中,若已有结点flag为1,说明此递归树中已访问过了,此时又出现该点,说明形成环,直接返回false;对该点标记为1,继续递归。在回溯过程中,对每个点标记为-1。对其他点构建递归树时,若遇到-1的点,则说明该点(该课程)肯定能选,直接返回true。对所有点都构造递归树后,返回true。
Dijkstra算法
Dijkstra 算法,又叫迪科斯彻算法(Dijkstra),解决的是单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。该算法依据的是最短路径的最优子结构性质,从起点开始,每一步都走最短的路径,并不断更新每个点到起点的最短距离,则可以得到每个点到起点的最短距离。实际上,迪科斯彻算法是A*算法的特殊情况,即h(n)等于0的情况,此时f(n)=g(n)。Dijkstra算法无法判断含负权边的图的最短路。
详细介绍参见:单源最短路径 Dijkstra标记算法
Bellman-Ford算法
Bellman-Ford算法又称贝尔曼-福特算法,是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
这篇文章对该算法的介绍非常详细,可参阅:Bellman-Ford algorithm
Floyd-Warshall算法
Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。通常可以在任何图中使用,包括有向图、带负权边的图。Floyd-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。该算法基本思想:如果从一个点到另一个点的直接路径不是最短的,那最短路肯定要经过第三个点。所以就用所有的点都当一次中间点插入到当前两点间最短路径中,来判断是不是任意两点之间经过中间点路程会减小。核心算法只有5行:
for(k=1;k<=n;k++) // k为“媒介节点”{一定要先枚举媒介节点}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j])
e[i][j]=e[i][k]+e[k][j]
参考https://baike.baidu.com/item/Floyd算法/291990和https://csruiliu.github.io/blog/2018-01-07-floyd-alg.html
多叉树的直径
我们可以选取一个起点, 然后我们找到最长的路径, 得到终点之后, 我们再从终点进行第二次dfs, 这样找到的最长路就是我们的直径
然后我们可以想一下
如果我们从u走到v恰好就是我们最后的直径这个是最好的
如果不是的话, 那么我们从v找, 我们可以保证v一定是直径的一个端点, 然后我们找到另一个点假设是w
然后我们可以知道, v−>w 和 u−>v 一定有一段是重合的, 所以我们最后就是直径
class Solution {
public:
#define LL long long
void floyd(int& n, vector<vector<LL>>& G) {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
}
// Floyd的板子
int solve(int n, vector<Interval>& Tree_edge, vector<int>& Edge_value) {
vector<vector<LL>> G(n + 1, vector<LL>(n + 1, INT_MAX));
// 初始值设置为INT_MAX, 不开longlong计算会有溢出
for (int i = 0; i < Tree_edge.size(); i++) {
G[Tree_edge[i].start][Tree_edge[i].end] =
G[Tree_edge[i].end][Tree_edge[i].start] = Edge_value[i];
}
// 邻接矩阵建图
floyd(n, G);
int maxx = INT_MIN;
for (auto& it : G) {
for (auto& it1 : it)
if (it1 != INT_MAX) maxx = max(maxx * 1ll, it1);
}
// 找到我们的最大值
return maxx;
}
};
堆
(133条消息) 数据结构之堆_Hidden.Blueee的博客-CSDN博客
堆的数据结构(二叉树)
堆的创建,堆的调整
java的优先级队列就是基于堆实现的
位运算
十进制转二进制字符串
Integer.toBinaryString()
Integer.toString(number, 2)
位运算加法器:
对于整数 a和 b:
在不考虑进位的情况下,其无进位加法结果为 a⊕b。
而所有需要进位的位为 a & b,进位后的进位结果为 (a & b) << 1。
public int add(int a, int b) {
while (b != 0) {
int carry = (a & b) << 1;
a = a ^ b;
b = carry;
}
return a;
}
java 数组切片:
// 数组
test_int = Arrays.copyOfRange(test_int, 1, 4);
//字符串
test_string = test_string.substring(1, 4);
//list
ArrayList<Integer> test_list_2 = new ArrayList<>(test_list.subList(1, 4));
十进制二进制转换
//int转换为二进制字符串
Integer.toBinaryString(5);
//二进制字符串转换为无符号int
int i=Integer.parseUnsignedInt("10010",2);
//有符号int
int i=Integer.parseInt("10010",2);
数组中数字出现的次数
剑指 Offer 56 - I. 数组中数字出现的次数 - 力扣(Leetcode)
这道题非常有意思,对于一个简单问题:
如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?
只需要将所有数字进行异或运算,最终结果即为那个数字
而本题中有两个数字都是唯一的,那么如果我们将这两个数字拆分到两个数组中,就变成了两个上述的简单问题,如何将数组拆分是一个难题,要注意:
- 两个只出现一次的数字在不同的组中;
- 相同的数字会被分到相同的组中。
我们首先将数组全部异或,得到的结果其实是两个独立数字的异或结果,对于该结果,值为1的位,说明这两个数字对应该位的值不同,那么可以将该位为1的数字分为1组,该位为0的数字分到另一组,需要注意的是,对于相同的两个数字,其肯定会被分到同一组。
class Solution {
public int[] singleNumbers(int[] nums) {
int ret = 0;
for (int n : nums) {
ret ^= n;
}
int div = 1;
while ((div & ret) == 0) {
div <<= 1;
}
int a = 0, b = 0;
for (int n : nums) {
if ((div & n) != 0) {
a ^= n;
} else {
b ^= n;
}
}
return new int[]{a, b};
}
}
数学
圆圈中最后剩下的数字
class Solution {
public int lastRemaining(int n, int m) {
return f(n, m);
}
public int f(int n, int m) {
if (n == 1) {
return 0;
}
int x = f(n - 1, m);
return (m + x) % n;
}
}