T1:约瑟夫游戏--
剑指 Offer 62. 圆圈中最后剩下的数字:
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
解:
法一:利用 循环链表的结构进行模拟 -- 通过27个测试用例后超时(这是ppt上推荐的方法)
12.关键:
(1)设置节点结构体(val+next指针)
(2)构造一个 不带头节点的 循环链表
(3)tmp指针每次移动m-1次后指向需要 删除的节点的上一个节点,然后通过 tmp->next = tmp->next->next 的方法 删除下一个节点
(4)终点条件: tmp = tmp->next 此时只剩下一个节点了
2.代码:
class Solution {
public:
//(1)定义链表结构
struct ListNode
{
public:
//构造函数:
ListNode(int x)
{
val = x;
}
int val;
ListNode* next;
};
int lastRemaining(int n, int m) {
//特殊 :
if(n==1)
{
return 0;
}
//考虑使用循环链表进行解决
//构造一个循环链表:
//利用0-n-1初始化一个 不带头节点的 循环链表
ListNode* head = new ListNode(0);
ListNode* tmp = head;
//(1)初始化:
for(int i=1;i<=n-1;i++)
{
ListNode* node = new ListNode(i);
tmp->next = node;
tmp = node;
}
tmp->next = head; // 好了,一个循环链表构造好了
//(2)开始模拟 这个约瑟夫 游戏
//每次最好找到需要删除的节点的前一个节点,或者用 快慢指针去找需要删除的节点
//所以每次移动2次,然后删除下一个节点即可
while(tmp->next != tmp)
{
//移动m-1次
for(int i=1;i<=m-1;i++)
{
tmp = tmp->next;
}
//--然后,删除下一个节点
tmp->next = tmp->next->next;
}
return tmp->val; //tmd超时
//有办法了,只是第一次去找之后的m-1个
}
};
法二:自然是通过数学公式 , 直接在一个数组vec上做文章:
1.关键:
(1) //我们要使用的数学方法,就是从结果0号位置,反推最开始在哪(讨论区)--具体可以去看一看leetcode的讨论区的想法
(2)反正最后剩余的 那一个元素“它”的新下标一定是0, 请问,最开始“它”的下标是哪一个?
(3)那么,就可以反推:ans =(ans+m) % i;//这里的i是 “上一轮游戏”的元素个数
2.代码:
class Solution {
public:
int lastRemaining(int n, int m) {
//我们要使用的数学方法,就是从结果0号位置,反推最开始在哪(讨论区)
//妙啊!!!!
//反正,最后一次的下标位置一定是0
//只要反推出第一次的 下标位置,不就是“它”最初的位置--也就是最初的数值了吗
int ans = 0;
for(int i=2;i<=n;i++)
{
ans=(ans+m)%i; //这里i是“上一次”剩余的 元素个数
}
return ans;
}
};
T587:凸包算法--graham算法--安装栅栏
给定一个数组 trees
,其中 trees[i] = [xi, yi]
表示树在花园中的位置。
你被要求用最短长度的绳子把整个花园围起来,因为绳子很贵。只有把 所有的树都围起来,花园才围得很好。
返回恰好位于围栏周边的树木的坐标。
解:
1.关键:
(1)复习 高等数学 中的 平面向量叉乘的概念
【平面向量】向量的叉积与三角形的面积 - Dylaaan的文章 - 知乎 https://zhuanlan.zhihu.com/p/34191849
(主要内容:aXb = a*b*Sin , 角度从a向量到b向量 逆时针为正, 坐标公式 a=(x1,y1),b=(x2,y2) , aXb = x1*y2 - x2*y1)
总的来说,这个坐标公式 aXb = x1*y2 - x2*y1的推导
第一步,先不管符号,主要看 模值 |aXb | = |x1*y2 - x2*y1|这个推到利用到cos与sin的关系即可
第二步, 正负 号的 判断,一个非常巧妙的方法就是 上面这张图中的 “不失一般性”,将2个向量放到 第一象限 进行考虑,利用y1 = k1 * x1,y2= k2* x2,最终只要比较“斜率”的大小 就可以 直到 b向量是否在 a向量的 逆时针方向了
(2)这里采用graham的算法,虽然课堂上的 文字描述 可以理解,但是 转化为 标准的代码语言还是有一些难度的,所以,这里考虑先参考官方的 解答代码
(3)预备的 辅助函数:
<1>cross(vec p, vec q, vecr )函数,返回一个int,计算向量 pq 和 qr的叉乘,传递3个点坐标参数,返回叉乘的结果
<2>dis_square(vec p,vec q) 函数,返回int, 计算2个坐标点之间距离的平方,传递2个点坐标参数,返回 距离的平方
(4)
<1>先考虑 tree的数量n < 4的特殊情况,直接返回,同时将bottom下标设置为0
<2>先找到y坐标最小的tree的下标给到bottom,任意一个y最小的都可以
<3>!!!!sort排序,从trees.begin()+1 一直到end,第三个lambda表达式作为 排序函数指针的 参数,
case1:diff相同,说明这两个点相对与 bottom是共线的,那么只要再按照距离排序即可
case2:diff>0 ,说明b再a的逆时针,所以a排序在前
<4>单独考虑最后 一条边 出现 共线的“那些tree”的情况
//我明白它的意思了,就是说
//当最后一条边上的共线的那些点时,
//原来按照 先逆时针角度 再距离的排序方法 ,顺序是近距离的在前
//然鹅,我们 在这一种情况下,需要的是 远距离在前
//所以 ,需要进行 类似于 对撞指针的 翻转
<5>好了,所有的准备工作都完成了,之后只要 利用一个stack栈结构,然后遍历排序号的trees顶点数组即可,每次比较栈顶的 2个元素 和 新遍历的那个顶点 之间的 旋转关系
<6>最后,从stack中取出的元素就是最终的答案
2.代码:
(非常抱歉,我的代码 好像有哪个地方出了一些问题,这里就直接把官方的 代码搬过来了,自己的代码我放到 哪个“笔记”模块中了)
class Solution {
public:
int cross(const vector & p, const vector & q, const vector & r) {
return (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]);
}
int distance(const vector & p, const vector & q) {
return (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]);
}
vector> outerTrees(vector> &trees) {
int n = trees.size();
if (n < 4) {
return trees;
}
int bottom = 0;
/* 找到 y 最小的点 bottom*/
for (int i = 0; i < n; i++) {
if (trees[i][1] < trees[bottom][1]) {
bottom = i;
}
}
swap(trees[bottom], trees[0]);
/* 以 bottom 原点,按照极坐标的角度大小进行排序 */
sort(trees.begin() + 1, trees.end(), [&](const vector & a, const vector & b) {
int diff = cross(trees[0], a, b);
if (diff == 0) {
return distance(trees[0], a) < distance(trees[0], b);
} else {
return diff > 0;
}
});
/* 对于凸包最后且在同一条直线的元素按照距离从大到小进行排序 */
int r = n - 1;
while (r >= 0 && cross(trees[0], trees[n - 1], trees[r]) == 0) {
r--;
}
for (int l = r + 1, h = n - 1; l < h; l++, h--) {
swap(trees[l], trees[h]);
}
stack st;
st.emplace(0);
st.emplace(1);
for (int i = 2; i < n; i++) {
int top = st.top();
st.pop();
/* 如果当前元素与栈顶的两个元素构成的向量顺时针旋转,则弹出栈顶元素 */
while (!st.empty() && cross(trees[st.top()], trees[top], trees[i]) < 0) {
top = st.top();
st.pop();
}
st.emplace(top);
st.emplace(i);
}
vector> res;
while (!st.empty()) {
res.emplace_back(trees[st.top()]);
st.pop();
}
return res;
}
};
T3:KMP字符串匹配相关的 问题:
28. 找出字符串中第一个匹配项的下标
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
解:
(以宫水三叶 的 讲解 作为 知识结构)
这个答主写得更好!
这个答主写的kmp跟实操的下标的关系对应的不是很好 建议参照:如何更好地理解和掌握 KMP 算法? - 海纳的回答 - 知乎 https://www.zhihu.com/question/21923021/answer/281346746
其中非常精辟的句子
no.1:其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。
no.2:具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。
no.3:KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组
PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。例如,对于”aba”,它的前缀集合为{”a”, ”ab”},后缀 集合为{”ba”, ”a”}。两个集合的交集为{”a”},那么长度最长的元素就是字符串”a”了,长 度为1,所以对于”aba”而言,它在PMT表中对应的值就是1。再比如,对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3。
其中要注意的一个技巧是,在把PMT进行向右偏移时,第0位的值,我们将其设成了-1,这只是为了编程的方便,并没有其他的意义。
1.关键:
(1)核心:就是 构建一个 和 匹配串子串 对应的 next数组,这个构建的构成 比较有技巧,需要进行 非常细致 且 熟练的 总结:
<1>初始值next[0] = 0,反正如果是在第0个字符不匹配,那么自然,(主串的指针一直向前移动1个位置),这时 子串的 指针一定也要 回到第0个字符
<2>下标变量j从0开始,下标变量i从1开始用
<3>如果str[j] == str[i] ,则next[i] = j+1; 而且 i++ 和 j++
(2)剩余的一些 分析 ,关键是 这个next数组的 构建 过程,自己 再仔细去研究一下那个 知乎 文章即可 !! 希望有一天可以 彻底悟透!
2.代码:
class Solution {
public:
int strStr(string haystack, string needle) {
//开始进阶到 KMP匹配算法
int size_needle = needle.size();
int size_hay = haystack.size();
vector next(size_needle,0);
get_next(next,needle); //得到next数组
//(1)这里 借鉴 知乎答主 海纳 的代码思路--写得非常好!
int i=0;
int j=0;
while(i < size_hay && j < size_needle)
{
if(j==-1 || haystack[i] == needle[j])
{
i++;
j++;
}
else
{
j = next[j]; //默认构建好了 next数组
}
}
if(j == needle.size())
{
return i-j;
}
else{
return -1;
}
}
//--剩下的工作就是 构建那个next 数组了
void get_next(vector& next,string str)
{
//这个函数 -- 根据 子串 获取到 next数组中的具体值
//悟了:next[j]==-1说明了一个问题:
//!!就是,首先,这个位置j的字符 和 主串中i的字符不相同
//而且,子串中要么这就是第0个字符,前面没有字符了,
//要么,前面的字符都和 这个i字符不相同
//所以,下一次,i++因为不要期望这个i有什么用了,j++,因为需要从-1到达0位置
next[0] = -1;
int i=0, j=-1;
while(i < str.size()-1)
{
if(j == -1 || str[j] == str[i])
{
i++;
j++;
next[i] = j;
}
else{
j = next[j];//这个位置的j不合适,需要让j回到上一个前缀合理的位置
}
}
}
};