3月31号 上午 数据结构课程中 引出的几个算法题目

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)

3月31号 上午 数据结构课程中 引出的几个算法题目_第1张图片

总的来说,这个坐标公式 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回到上一个前缀合理的位置
            }
        }
    }
};

你可能感兴趣的:(数据结构)