leetcode总结

数据库知识

一.内连接、外连接

  • 详细讲解在博客链接:link

二.数据库在通过连接两张或多张表来返回记录时,都会生成一张中间的临时表,然后再将这张临时表返回给用户。 在使用left jion时,on和where条件的区别如下:

  • on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。
  • where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。
# Write your MySQL query statement below
select a.FirstName, a.LastName, b.City, b.State from Person a left join Address b on a.PersonId = b.PersonId;

数论问题

欧几里得算法

给定两个整数a,b,求他们的最大公约数。
时间复杂度:log(N);
leetcode总结_第1张图片

扩展欧几里得算法

裴蜀定理:给定任意两个正整数a,b,假设d是他们的最大公约数,d=(a,b);则存在一定整数x,y,使得ax+by=d.

扩展欧几里得算法求出x,y

int exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
     x=1;y=0;
     return a;
    }
    int d=exgcd(b,a%b,y,x);
    y-=(a/b)*x;
    return d;
}

质因数分解

算数基本定理

给定一个正整数n,则存在一个唯一的分解使n=p1^a1*······* pm^am,其中(p1···pm是不相等的质数,a1···am是质数的系数)。

质因数分解代码

cout<

拓展:质因数分解的应用

  • 时间复杂度是O(logn)~O(sqrt(n)).
  1. n共有多少个约数;
//约数是指能整除n的所有的数,约数的个数=(a1+1)*(a2+1)*······(am+1)
  1. 欧拉数:小于n且与n互质的数的个数。
phi(n) = n *(1-1/p1)*(1-1/p2)*······*(1-1/pm);
假设n是质数,n=p,phi(n)=phi(p) =p-1;p(1-1/p)=p-1;
假设n=p^k ; phi(n) = n - n/p = n(1-1/p);
假设n=p1^a1*······* pm^am,证明如下:
n - n/p1 - n/p2 - n/p3 - ······· - n / pm + n / p1p2 + n / p1p3 + ······ - n / p1p2p3 -·····+n / p1p2p3p4 -·····
= n *(1-1/p1)*(1-1/p2)*······*(1-1/pm);

筛素数

最low筛素数

复杂度为O(n*sqrt(n))

int f1(int n)
{
     for(int i=2;i<=n;i++)
     {
         bool flag=False;
          for(int j=2;j*j<=i;j++)
          {
               if(i%j==0)
               {
                   flag=True;
                   break;
                }
          }
         if(!flag) cout<

普通筛素数

基本思想:素数的倍数一定不是素数
实现方法:用一个长度为N+1的数组保存信息(0表示素数,1表示非素数),先假设所有的数都是素数(初始化为0),从第一个素数2开始,把2的倍数都标记为非素数(置为1),一直到大于N;然后进行下一趟,找到2后面的下一个素数3,进行同样的处理,直到最后,数组中依然为0的数即为素数。
说明:整数1特殊处理即可

//循环次数:n/2+n/3+n/4+······+n/n
时间复杂度为O(nln(n));

bool st[N];
int f2(int n)
{
     for(int i=2;i<=n;i++)
     {
          if(st[i]) continue;
          primes[cnt++]=i;
          for(int j=i+i;j<=n;j+=i)
               st[j]=true;
     }
     for(int i=0;i

线性筛素数

时间复杂度O(n)

void f3(int n)
{
    for(int i=2;i<=n;i++)
    {
        if (!st[i]) primes[cnt++]=i;
        for(int j=0;j

欧拉函数

int euler[N];
void f3(int n)
{
    euler[1]=1;
    for(int i=2;i<=n;i++)
    {
        if (!st[i])
        {
             primes[cnt++]=i;
             euler[i]=i-1;
        }
        for(int j=0;j

小于n且和n互质的数的和=euler(n)*n/2

//因为a和n互质,则n-a和n也互质

邮递员问题(题号:462)

/*
x0,x1,···,xn-1
|x0-x|+|x1-x|+···+|xn-1-1|

|x0-x|+|x(n-1)-x|
|x1-x|+|x(n-2)-x|
···
则数的个数若是奇数,那么选择中间的点(中位数);
若数的个数是偶数,那么选择中间两点的任意中间位置。
*/

代码详见leetcode

动态规划

动态规划算法的核心就是记住已经解决过的子问题的解。

为何要对 1000000007 取模?

  • 1000000007是一个质数。
  • int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。
  • int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出。
  • 所以在大数相乘的时候,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。

BFS和DFS问题

leetcode:279:完全平方数;111:树的最小深度;FloodFill深搜:733;

异或的性质

  • a^a=0;
  • 0^a=a;

&的性质

n&(-n)的结果为最低位为1的值。

贪心算法

贪婪算法(Greedy algorithm)是一种对某些求最优解问题的更简单、更迅速的设计技术。用贪婪法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。

  • leetcode:421
    1.Trie树
struct Node{
	int son[2];
};
vector nodes;

动态规划算法

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法–动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。

leetcode:376

unique函数的用法:

unique函数属于STL中比较常用函数,它的功能是元素去重。即”删除”序列中所有相邻的重复元素(只保留一个)。此处的删除,并不是真的删除,而是指重复元素的位置被不重复的元素给占领了(详细情况,下面会讲)。由于它”删除”的是相邻的重复元素,所以在使用unique函数之前,一般都会将目标序列进行排序。
比如:
在这里插入图片描述

leetcode链表专题

题号:61、143、21 、160

leetcode二分法

  • 模板
    二分模板一共有两个,分别适用于不同情况。
    算法思路:假设目标值在闭区间[l, r]中, 每次将区间长度缩小一半,当l = r时,我们就找到了目标值。
    版本1
    当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。
    版本2
    当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。

二分题目的特点

  • 70%的二分题目具有单调性。
  • 95%存在两段性的性质;比如某一段满足该性质,另一段不满足该性质。(具体讲解见大雪菜leetcode暑期打卡二分专题)

二分的流程

  1. 确定二分边界,l和r
  2. 编写二分的代码框架
  3. 设计一个check(性质)
  4. 判断一下区间如何更新
  5. 如果更新方程写的是l=mid,r=mid-1,那么就在算mid时加上1
  • 题库
  • 69、35、34、153、278、287、162、275

链表专题

  • 题号:19、237、83、61、24(常考)、206(必考)、92、160(相交链表)、142、148
  • 做法
  1. 画图
  2. 有可能删除头节点的链表问题,可以创建一个虚拟头节点,该节点指向真正的头节点。就无需单独处理头节点删除的情况。
  3. 考虑双指针的方式,减少时间消耗。
  4. 删除节点写delete,不写的话可能会有内存泄漏。

快速排序(注意最好最差情况下的时间复杂度)

①先从队尾开始向前扫描且当low < high时,如果a[high] > tmp,则high–,但如果a[high] < tmp,则将high的值赋值给low,即arr[low] = a[high],同时要转换数组扫描的方式,即需要从队首开始向队尾进行扫描了
②同理,当从队首开始向队尾进行扫描时,如果a[low] < tmp,则low++,但如果a[low] > tmp了,则就需要将low位置的值赋值给high位置,即arr[low] = arr[high],同时将数组扫描方式换为由队尾向队首进行扫描.
③不断重复①和②,知道low>=high时(其实是low=high),low或high的位置就是该基准数据在数组中的正确索引位置.
快速排序详解转载链接

//单链表的快速排序:
class Solution {
public:
        ListNode* sortList(ListNode* head) {
		quickSort(head, nullptr);
		return head;
	}

	void quickSort(ListNode* head, ListNode* tail) {
		if (head == tail || head->next == nullptr) {
			return;
		}

		ListNode* mid = partition(head, tail);

		quickSort(head, mid);
		quickSort(mid->next, tail);
	}

	ListNode* partition(ListNode* head, ListNode * tail) {

		int pivot = head->val;
		ListNode* s = head;
		ListNode* cur = head->next;
		while (cur != nullptr && cur != tail)
		{
			if (cur->val < pivot) {
				s = s->next;
				swap(s, cur);
			}
			cur = cur->next;
		}
		swap(s, head);
		return s;
	}


	void swap(ListNode* a, ListNode* b) {
		int temp = a->val;
		a->val = b->val;
		b->val = temp;
	}

};

归并排序

归并排序详解转载

树专题

题号:98(两种方法,自底向上和自上到下)、94、101、105、102(层序遍历)、236(二叉树的公共祖先)、543和124类似(寻找最大路径,543无权值,124有权值)、173、297(序列化和反序列化)、994(腐烂的橘子:广度优先遍历)
二叉搜索树(BST):每一个节点的值严格大于所有左子树,并且严格小于所有的右子树。

  • 常用方法:队列(层序遍历)、栈(中序遍历等)、递归方法(比如:树的最大深度或最小深度)

字符串专题

字符串最大长度为512M。
题号:38、49(排序和哈希表的结合,转载map和unordered_map的差别和使用)、151、165、929、5(寻找回文字符串)、6(z字形变换,两种解法)、3(无重复字符的最长子串)、208(trietree字典树/前缀树的实现)、273将整数转化成英文

DFS(深搜)和回溯

搜索不一定等于递归
八皇后问题和数独问题目前已有了解决办法,统称精确覆盖问题(Dancing Links,十字链表)
题号:17(电话号码的字母组合)、79(单词搜索,步骤:枚举起点;从起点开始,依次搜索下一个点的位置;在枚举的过程中,要保证和目标单词匹配,一般需要恢复原来的状态)、46(全排列)、47(包含重复数字的全排列(逻辑难)、78(子集,可以用迭代(二进制)和递归两种方法)、90(两种递归思想)、216(组合总和)、52(八皇后问题)、37(数独问题)、

  • 473(火柴拼正方形):剪枝方法
  1. 从大到小枚举所有边
  2. 每条边内部的木棒长度规定成从大到小
  3. 如果当前木棒拼接失败,且是当前边的第一个,则直接剪掉该分支。
  4. 如果当前木棒拼接失败,则跳过接下来所有长度相同的木棒。
  5. 如果当前木棒拼接失败,且是当前边的最后一个,则直接剪掉该分支。

滑动窗口、双指针、单调队列和单调栈

题号:

  • 双指针问题
    167(两数之和 II - 输入有序数组):考点是双指针算法的优化方式
    88(合并两个有序数组,应用了归并排序的方法):双指针
    26 (删除排序数组中的重复项):双指针
    76(最小覆盖子串):滑动窗口算法,即特殊的双指针算法,使用了hash表
    32(最长有效括号):双指针或者栈的方法
    155(最小栈):类似于前缀和,存的是所有前缀里的最小值
  • 单调栈问题(830单调栈的模板题)
    查找每个数左/右侧第一个比它小/大的数(90%)
    84(柱状图中最大的矩形):枚举所有柱形的上边界,作为整个矩形的上边界,然后求出左右边界单调栈问题:该操作即为找出左边和右边离它最近的,比它小的柱形。)。
    42(接雨水:困难):求左边第一个比它大的位置
  • 单调队列问题(239单调队列的模板题)
    查找滑动窗口中的最值
    239(滑动窗口最大值):可以将一个环展开成长度是2n的量。
    918(环形子数组最大值)

基本数据结构:堆、平衡树、哈希表、

  • 哈希表问题
    1(两数之和)用到了hash.count(),返回 hash_map 中其键与参数指定的键匹配的元素的数量。
    187(重复的DNA序列):插入一个字符串后,统计字符串出现的次数。
    706(设计哈希映射):拉链法实现哈希表,使用vector>> h。list用法总结链接转载
    652(寻找重复的子树):两个哈希表,将子树的string类型的key映射为int类型,时间复杂度为O(N);而不映射的话,string的字符串复制是O(N),而总的复杂度为O(N*2);
    560(和为k的子数组):前缀和、哈希;若该题改成和<=k的子数组的个数,则需要手写平衡树(有效应对动态维护有序链表的问题)
    795(主要针对前缀和的模板题)

  • 并查集(时间效率O(1))
    步骤:

    1. 合并两个集合;
    2. 判断两个点是否在同一个集合中;
      并查集存在两个优化:路径压缩和按秩合并

    547(朋友圈问题:连通图问题):可以使用深度优先遍历、广度优先遍历以及并查集(路径压缩)的方法.
    684(冗余连接):返回一条可以删去的边,使得结果图是一个有着N个节点的树。


  • 大根堆要求根节点的关键字要既大于或等于左子树的关键字,又要大于或等于右子树的关键字。

  • 堆排序的过程

  1. 构造一个大根堆,取堆顶数字。
  2. 将剩下的数字构建一个大根堆,取堆顶数字。
  3. 重复以上操作,直到取完堆中的数字,最终得到一个从大到小的序列。
void swap(int k[],int i,int j)
{
	int temp;
	temp=k[i];
	k[i]=k[j];
	k[j]=temp;
}
void HeapAdjust(int k[],int s,int n)
{
	int i;
	int temp=k[s];
	for(i=2*s;i<=n;i*=2)
	{
		if(i=k[i])
		{
			break;
		}
		k[s]=k[i];
		s=i;
		//swap(k,s,i);
	}
	k[s]=temp;
}
void HeapSort(int k[],int n)
{
	int i;
	for(int i=n/2;i>0;i--)
	{
		HeapAdjust(k,i,n);
	}
	for(i=n;i>1;i--)
	{
		swap(k,1,i);
		HeapAdjust(k,1,i-1);
	}
}
int main()
{
	int i,a[10]={-1,5,2,6,0,3,9,1,7,4};
	HeapSort(a,9);
	printf("jieguo:");
	for(i=1;i<10;i++)
	{
		cout<
  • 初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。
  • 基本操作:
    1. 查找最大值O(1)
    2. 插入一个数O(logn)
    3. 删除一个数O(logn)
    4. 修改一个数O(logn)

但是如果是c++ stl中的堆priority_queue,则不能修改一个数,而且只能删除堆顶元素。
堆的两个基本操作down()和up().

题号:
692(前K个高频单词):使用小根堆(c++默认为大根堆)来做。注意字典序和加负号的问题。
295(数据流的中位数):使用大根堆和小根堆的方法O(logn);平衡二叉搜索树
priority_queue lo; // max heap
priority_queue hi; // min heap

  • 平衡二叉搜索树
    352(将数据流变为多个不相交区间):用map来维护平衡树,map(即平衡二叉树)的遍历是O(n)。分为四种情况来讨论。

动态规划问题

  • 关键是得出状态转移方程
  • 常用思路:
    从集合的角度来考虑DP问题。
    用某一类数来代表不同的数,将一组数分成不同类别的集合。
  • 题号:
  • 线性DP:
    53(最大子序和):状态转移方程为f[i]=max(f[i-1],0)+nums[i];
    120(三角形最小路径和):可以用滚动数组节省空间。
    63(不同路径2):动态规划。
    91(解码方法)
    198(打家劫舍):用两种不同的状态来表示: f[i]表示在前i个数中选,所有不选nums[i]的选法的最大值;g[i]表示在前i个数中选,所有选nums[i]的选法的最大值。
    300(经典题: 最长上升子序列):动态规划时间复杂度O(n^2);贪心算法时间复杂度O(nlogn);
    896(最长上升子序列2)
    72(经典题: 编辑距离):分为删除操作、插入操作以及取代操作,记得初始化f[i][0]和f[0][j];
    10(正则表达式匹配:困难):状态集合:所有字符串s中前i个字符与字符串前j个字符是否相匹配。
  • 区间DP:
    664(区间DP问题:奇怪的打印机):区间DP常用的状态表示为f[L,R](集合:将区间L-R染成最终样子的方式;属性:最小需要多少步)--------困难( 不太懂)

背包DP问题

leetcode518(经典题(全背包问题): 零钱兑换):状态计算为f[j]+=f[j-c];

Acwing 2(01背包问题):f[i][j]的含义是前i个物体的体积不大于j的最大价值;优化:可以变为一维数组。
Acwing 3(完全背包问题);
Acwing 4(多重背包问题1):第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
Acwing 5(多重背包问题2):相比于第4题,该题的数据量变大,因此需要用二进制优化的方式,将问题转化为01背包问题.
Acwing 6(多重背包问题3):相比于第5题,该题的数据量更大,因此使用单调队列的方式,用滑动窗口来解决该问题。
Acwing 7(混合背包问题):包含01背包,完全背包,以及多重背包问题。
Acwing 8(二维费用的背包问题):多加入了一个重量的限制条件
Acwing 9(分组背包问题):从不同的分组中选择一个物品,状态转移与01背包类似
Acwing 10(有依赖的背包问题 困难:没看太懂):很难,使用01背包,分组背包结合,使用递归方法
Acwing 11(背包问题求方案数):f[j]的含义为前i个物品的体积等于j的最大价值,又定义一个g[i]表示前i个物品的体积等于j的方案数,其中f[i]都初始化为-INF,g[0]初始化为0.
Acwing 12(背包问题求具体方案):与01背包不同的是从后往前枚举求f[i],然后从前往后输出最大价值的字典序

由数据范围反推算法复杂度以及算法内容

参见ACWING技术分享

贪心专题

不考虑所有的情况去达到最优解。

  • 题号:
    860(柠檬水找零)
    392 (判断子序列)
    455 (分发饼干)
    55 (跳跃游戏)
    45 (跳跃游戏2)
    376 (摆动序列):寻找拐点,就是寻找局部最大值和局部最小值。
    406(根据身高重建队列):个子高的看不见个子低的,注意sort()和vector的insert(i,a)用法,i表示插入的位置,a表示插入的值。vector中insert函数的用法转载
    452(用最小数量的箭引爆气球 贪心经典题):区间判断,比较区间的最右端;
    402(移掉k位数字 经典的贪心问题):其实就是单调递增栈的问题;
    134 (加油站问题超级经典的贪心算法问题):暴力搜索后再优化
    剪绳子:整数规划问题,只可能有3和2两种数,且2的个数不超过两个。

每日打卡题

面试题59-2(队列的最大值):单调递减的双端队列
面试题33(二叉搜索树的后序遍历序列):可以使用单调栈的方法,逆序遍历;也可以使用递归的方法;
945(使数组唯一的最小增量):贪心算法;

队列等数据结构的实现题

剑指offer:用两个栈实现队列;用一个/两个队列实现栈;
剑指offer:包含min函数的栈:定义两个栈,其中一个栈保存分块递增区间的最小值。
剑指offer:栈的压入、弹出序列;

unordered_map和unordered_set自定义哈希函数

struct HashPair {
     size_t operator()(const pair &key) const noexcept
	 {
		 return size_t(key.first)*100000007 + key.second;
	 }
 };

为什么哈希表的设计最好选用质数

减少哈希冲突
哈希表选用素数

阿里笔试编程题记录

  • 动态规划
#include
#include
#include
#include
#include
using namespace std;
int main()
{
	int n;
	cin>>n;
	vector> a(3,vector(n));
	for(int i=0;i<3;i++)
		for(int j=0;j>a[i][j];
	vector> f(3,vector(n,INT_MAX));
	f[0][0]=0;
	f[1][0]=0;
	f[2][0]=0;
	for(int i=1;i

#include
#include
#include
#include
using namespace std;
const int N=600;
int main()
{
int n,m,q;
cin>>n>>m>>q;
vector A(n,vector(m));
unordered_map hash_row,hash_col;
for(int i=0;i for(int j=0;j {
cin>>A[i][j];
if(A[i][j]!=0)
{
hash_row[i].push_back(j);
hash_col[j].push_back(i);
}
}
int x,y;
int a[N][2];
for(int i=0;i {
cin>>a[i][0]>>a[i][1];
}
for(int i=0;i {
//cin>>x>>y;
int d;
x=a[i][0]-1;
y=a[i][1]-1;
//x=x-1;
//y=y-1;
if(A[x][y]!=0) cout< else
{
if(hash_row[x].size()>1)
{
d=(A[x][hash_row[x][0]]-A[x][hash_row[x][1]])/(hash_row[x][0]-hash_row[x][1]);
A[x][y]=A[x][hash_row[x][0]]-d*(hash_row[x][0]-y);
cout< hash_row[x].push_back(y);
}
else if(hash_row[x].size()<=1&&hash_col[y].size()>1)
{
d=(A[hash_col[y][1]][y]-A[hash_col[y][0]][y])/(hash_col[y][1]-hash_col[y][0]);
A[x][y]=A[hash_col[y][1]][y]-d*(hash_col[y][1]-x);
cout< hash_col[y].push_back(x);
}
else cout<<“Unknown”< }

	}

return 0;

}

你可能感兴趣的:(总结)