2020年3月

[leetcode]125-验证回文串

这道题本身是非常简单的,但是由于审题不仔细,忽略一个细节,导致提交了6次才成功,要检讨!

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/valid-palindrome

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

  • 输入: "A man, a plan, a canal: Panama"
  • 输出: true

示例 2:

  • 输入: "race a car"
  • 输出: false

二、题解

思路:使用双指针,一个从前往后遍历,一个后从往前遍历。遇到非数字和字符就跳过,如果发现两个指针的字符不一样就返回false,直到两个指针汇合,返回true。

要特别注意到的是回文串也包含了数字,我就是因为没看到还包含了数字,所以提交了6次才过。一直卡在测试案例"0P"上,注意是“零+P”,不是“欧+P”。网页上的显示0和O太像了,我看成了"OP",本地测没问题,提交就有问题,最后失败了5次才发现,看评论区才知道大家都被这个输入坑了!!

三、代码

// 判断是否是字符或数字
bool isCharOrNum(char ch) {
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9');
}

class Solution {
public:
    bool isPalindrome(string s) {
        int i = 0, j = s.size() - 1;
        while (i < j) {
            // 跳过非字符
            if (!isCharOrNum(s[j])) {
                j--;
                continue;
            }
            if (!isCharOrNum(s[i])) {
                i++;
                continue;
            }

            // 忽略大小写
            if (tolower(s[i]) != tolower(s[j]))
                return false;
            i++, j--;
        }
        return true;
    }
};

单元测试:

#include <gtest/gtest.h>

TEST(leetcode, demo) {
    string s1("A man, a plan, a canal: Panama"), s2("race a car"), s3("OP");
    Solution s;
    EXPECT_TRUE(s.isPalindrome(s1));
    EXPECT_FALSE(s.isPalindrome(s2));
    EXPECT_FALSE(s.isPalindrome(s3));
}

TEST(leetcode, OP) {
    string s1("PO"), s2("0P");
    Solution s;
    EXPECT_FALSE(s.isPalindrome(s1));
    EXPECT_FALSE(s.isPalindrome(s2));
}

int main() {
    ::testing::InitGoogleTest();
    return RUN_ALL_TESTS();
}

做代码优化,发现代码中获取系统CPU核数是通过system调用命令得到的,想想最近被system支配的恐惧,果断改掉。

linux c中获取CPU核数的函数原语有两个:

#include <sys/sysinfo.h>
get_nprocs_conf();
get_nprocs();

第二个函数是返回当前可用的CPU数量,不可用的意思是CPU HANG住了。如果安装了man page,可以直接在man page中找到用法。

#include <stdio.h>
#include <sys/sysinfo.h>

int
main(int argc, char *argv[])
{
   printf("This system has %d processors configured and "
           "%d processors available.\n",
           get_nprocs_conf(), get_nprocs());
   return 0;
}

一、问题现象

调用命令的时候,出现报错:

cannot create temp file for here-document: No space left on device

从错误的日志来看,应该是磁盘空间不足了。但是执行df -h看磁盘都是有剩余的:

上次遇到过一个类似的现象是,文件删除,但是容量没有归还。怀疑可能是同样的问题,但是执行lsof | grep delete看并没有被删除但没有归还空间的文件。

经过询问大佬后,说可能是inode节点满了导致的,于是执行df -i看了一下还真是:

可以看到,磁盘总共26w个inode节点全部使用完了。再通过find查找所有文件,确实是看到了26w个文件:

二、结论

经过下一步分析,发现是设备在进行profile抽样,大量的抽样文件打到了/var目录下,导致磁盘占满。

因此判断,问题原因是磁盘文件数量太多,占满了inode节点导致的。

一、slowhttp攻击

slowhttp攻击的意思是客户端使用非常慢的速度发送数据到服务端,例如每秒发送1个字节头部或body,导致服务端连接长时间占用连接,当这种连接多了之后,服务端资源就会耗尽。

以下就是一个慢攻击测试案例,当1000个客户端连接缓慢发送HTTP头部到服务端后,服务器已经变成了不可用状态:

前端提示资源无法访问:

如何测试服务是否存在慢攻击漏洞

下载测试工具slowhttptest:https://github.com/shekyan/slowhttptest,根据文档编译安装。

然后执行:

slowhttptest -c 1000 -H -i 10 -r 200 -t GET -u https://服务器地址/页面 -x 24 -p 3

控制台会每5秒打印一次连接状态,可以持续监控,如果隔了几十秒后,连接还一直没有断开,说明服务端是存在漏洞的。

二、apache环境下修复

我们的web服务器是使用的apache,根据网上提供的方法,安装mod_reqtimeout模块,再添加以下配置:

<IfModule reqtimeout_module>
    RequestReadTimeout header=5-40,MinRate=500 body=20,MinRate=500
</IfModule>

重启apache,问题就解决了。再次使用测试工具测试,连接在建立后5秒左右会被断开。

三、参考

SlowHTTPTest-慢速DoS攻击

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/the-masseuse-lcci

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

注意:本题相对原题稍作改动

示例 1:

  • 输入: [1,2,3,1]
  • 输出: 4
  • 解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

示例 2:

  • 输入: [2,7,9,3,1]
  • 输出: 12
  • 解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

示例 3:

  • 输入: [2,1,4,5,3,1,1,3]
  • 输出: 12
  • 解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。

二、题解

这道题目和198-打家劫舍几乎完全一样,思路也完全一样,代码都不用做太多更改。

思路:对于第i个用户,有2种策略选择,预约或不预约。

  • 如果预约,那么说明上一个用户不能预约,只能取上上个用户的最优解加上当前用户的预约时长。
  • 如果不预约,最优解就是上一个用户的最优解。

总结出来的状态转移(递推)方程:

$$dp[i] = max(dp[i-1], dp[i-2]+nums[i]])$$

三、代码实现

class Solution {
public:
    int massage(vector<int> &nums) {
        int pprev, prev, output, i;
        output = prev = pprev = 0;

        if (nums.empty()) {
            return 0;
        } else if (nums.size() == 1) {
            return nums[0];
        }

        prev = nums[0];
        for (i = 1; i < nums.size(); i++) {
            output = max(prev, pprev + nums[i]);
            pprev = prev;
            prev = output;
        }

        return output;
    }
};

单元测试:

TEST(leetcode, demo) {
    Solution s;
    // leetcode样本数据
    vector<int> v1{1, 2, 3, 1}, v2{2, 7, 9, 3, 1}, v3{2, 1, 4, 5, 3, 1, 1, 3};
    // 特殊数据
    vector<int> v4{1}, v5{1, 2}, v6;

    EXPECT_EQ(s.massage(v1), 4);
    EXPECT_EQ(s.massage(v2), 12);
    EXPECT_EQ(s.massage(v3), 12);

    // 1个元素的数组
    EXPECT_EQ(s.massage(v4), 1);
    // 2个元素的数组
    EXPECT_EQ(s.massage(v5), 2);
    // 空数组
    EXPECT_EQ(s.massage(v6), 0);
}

头一回打败全部打败100%(其实是新题提交的人太少了):

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/middle-of-the-linked-list

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例1:

  • 输入:[1,2,3,4,5]
  • 输出:此列表中的结点 3 (序列化形式:[3,4,5])
  • 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL

示例2:

  • 输入:[1,2,3,4,5,6]
  • 输出:此列表中的结点 4 (序列化形式:[4,5,6])
  • 由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

提示:

给定链表的结点数介于 1100 之间。

二、题解

使用快慢指针,快指针每次走2步,慢指针每次走1步。当快指针指向null的时候慢指针就是中间节点。

问题的一个关键点是:如果有两个中间节点,返回第二个。因此要当快指针的下一个节点指向null时结束,否则结束条件是快指针的下一个节点是null。

三、代码

class Solution {
public:
    ListNode *middleNode(ListNode *head) {
        ListNode *fast, *slow;

        // 链表为空或者只有一个节点
        if (head == nullptr || head->next == nullptr) {
            return head;
        }

        // 快慢指针初始化
        fast = head->next;
        slow = head;

        while (fast != nullptr) {
            // 快慢指针分别走一步
            fast = fast->next;
            slow = slow->next;

            // 快指针在没有到达末尾的时候再走一步
            if (fast) {
                fast = fast->next;
            }
        }

        return slow;
    }
};

测试案例:

TEST(leetcode, demo) {
    int i;
    Solution s;
    ListNode *nodes[6];

    for (i = 0; i < 6; i++) {
        nodes[i] = new ListNode(i);
    }

    // 测试空链表
    EXPECT_EQ(nullptr, s.middleNode(nullptr));
    // 测试只有一个节点的链表
    EXPECT_EQ(nodes[5], s.middleNode(nodes[5]));
    // 测试有两个节点的链表
    EXPECT_EQ(nodes[4], s.middleNode(nodes[4]));
    // 测试n个节点的链表,n为奇数
    EXPECT_EQ(nodes[1], s.middleNode(nodes[1]));
    // 测试n个节点的链表,n为偶数
    EXPECT_EQ(nodes[0], s.middleNode(nodes[0]));
}

int main() {
    ::testing::InitGoogleTest();
    return RUN_ALL_TESTS();
}

一、我为什么不喜欢system和popen

要说到我为什么不喜欢system和popen这两个函数,这个说来就话长了。最开始,我还是很喜欢用这两个函数的,直到后来发现了太多因为滥用导致的程序异常后,它们就逐渐被我打入了冷宫。我认为一个设计良好的程序,完全是可以避开这两个函数的。这不,一周之内,我就收到了两个因为它们导致的BUG。

与其说我不喜欢这两个函数,倒不如说是不喜欢代码作者在不完全考虑好异常的情况下就使用它们^_^。

问题的现象是:线上程序功能失效,经过一番排查后,发现很多popen和system调用命令报错的日志。错误码是12,12的宏定义是ENOMEM,说明是无法分配足够的内存导致命令执行失败了。

strerror(ENOMEM) = "Alloc memory fail"

但是实际上设备的剩余内存还有好几百兆,所以这个错误有点让人摸不着头脑了。在网上查询了一波后,几乎全是说创建子进程时子进程完全拷贝了父进程的内存导致的。虽然我比较认同这个观点,因为这两个函数底层实现都是创建子进程执行命令,但是并没有一个人能讲述清楚为什么创建子进程时会拷贝父亲内存。

因为在学习多进程时听到最多的一句话是“读时共享,写时复制”,也就是说,子进程创建的时候,是共享父亲的内存的,不会完全拷贝内存到子进程,直到有数据修改才复制。这前后就逻辑就相违背了。

为了搞清楚这个问题,在google查了很久,也没有找到满意的答案。最后动手实践了一下,跟踪程序调用,发现是执行system时,程序并没有调用mmap来映射父进程的内存地址,想来system确实是不支持这个机制(虽然不知道结论是否正确,但从现象和调试过程来看,比较靠谱)。

二、strace调试

编写了一个简单的system调用程序:

#include <stdlib.h>

int main() {
    system("echo helloworld >> ~/test.txt");
    return 0;
}

编译,通过strace调用:

输出使用两个框框起来了,其中第一个框里面出现了大量的mmapmunmap,这些都是strace调用system命令产生的,不是system程序中的system()函数产生的。因为strace调试程序也是fork() + execve()来实现的(这个从第一行的输出就能看出来),这些mmap调用就是父子进程在映射共享内存,也就说明了正常情况下创建子进程确实是遵循“读时共享,写时复制”的。

但是重点是第二个红框标出来的部分,这里才是system程序执行部分。可以看到,这里的system()函数是直接通过clone来创建新进程的,创建完成后父进程就调用wait等待子进程退出了,并没有执行mmap这些函数来共享父进程内存,因此也就不支持COW原则。

三、参考

ENOMEM from popen() for system(), while there is enough memory

一、题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

  • 输入:arr = [3,2,1], k = 2
  • 输出:[1,2] 或者 [2,1]

示例 2:

  • 输入:arr = [0,1,2,1], k = 1
  • 输出:[0]

限制:

  • 0 <= k <= arr.length <= 10000
  • 0 <= arr[i] <= 10000

二、题解

解法一:排序

一个最简单的思路是先对所有元素排序,然后输出前面k个元素。使用快速排序的话,时间复杂度O(nlog(n))。

解法二:时间复杂度为O(n)的快速排序

快速排序的特点是:每次排序后,标杆元素左边的元素都比它小,右边的元素都比它大。因此可以利用这一个特性,对数组中的元素进行部分排序,直到返回的标杆元素下标等于k,那么这个标杆元素的左边就是所有需要的目标数据。

相较于快排来说,只需要对目标区间(标杆元素左边或者右边)进行排序就行了,无需对左右两边的区间都进行排序。因此时间复杂度降为O(n)。

解法三:最大堆

最大堆的性质是:堆上层的元素都大于等于下层元素。因此,只要维护一个大小为k的最大堆,循环遍历数组,如果元素大于堆顶元素,就替换掉堆顶元素,重新建堆。堆里面保存的就是k个最小的元素了。时间复杂度O(nlog(k)),空间复杂度O(k)。

三、代码:

3.1 基于快排思想的算法

class Solution {
private:
    int partition(vector<int> &arr, int left, int right) {
        int i, j, key;

        if (left >= right)
            return left;

        i = left;
        j = right;

        key = arr[i];
        while (i < j) {
            while (i < j && arr[j] >= key) {
                j--;
            }
            if (i < j && arr[j] < key) {
                arr[i] = arr[j];
            }

            while (i < j && arr[i] <= key) {
                i++;
            }
            if (i < j && arr[i] > key) {
                arr[j] = arr[i];
            }
        }
        arr[i] = key;

        return i;
    }

public:
    vector<int> getLeastNumbers(vector<int> &arr, int k) {
        int idx, left, right;
        vector<int> output;

        if (arr.size() <= k)
            return arr;
        if (arr.size() == 0)
            return output;

        left = 0;
        right = arr.size() - 1;
        // 循环分区,找到第k个标杆元素
        idx = partition(arr, left, right);
        while (idx != k) {
            if (idx > k) {
                // 标杆元素的下标大于k,要在左区间继续找
                right = idx - 1;
                idx = partition(arr, left, right);
            } else {
                // 标杆元素的下标小于k,要在右区间继续找
                left = idx + 1;
                idx = partition(arr, left, right);
            }
        }

        // 预分配空间,不要直接push_back,否则会浪费很多重新开辟空间的时间
        output.reserve(k);
        for (idx = 0; idx < k; idx++) {
            output.push_back(arr[idx]);
        }

        return output;
    }
};

3.2 使用最大堆

class Solution {
private:
    // 执行最大堆化
    void maxHeapify(vector<int> &v, int idx) {
        int mid, i, left, right, largest;
        left = (idx * 2) + 1;
        right = left + 1;

        if (left < v.size() && v[left] > v[idx])
            largest = left;
        else
            largest = idx;

        if (right < v.size() && v[right] > v[largest])
            largest = right;

        if (largest != idx) {
            swap(v[largest], v[idx]);
            maxHeapify(v, largest);
        }
    }
public:
    vector<int> getLeastNumbers(vector<int> &arr, int k) {
        int i, j, len;
        vector<int> output;

        if (k >= arr.size())
            return arr;

        if (k == 0)
            return output;

        len = 0;
        output.reserve(k);

        for (i = 0; i < arr.size(); i++) {
            if (len < k) {
                // 当最大堆的元素个数小于k时,直接插入到堆
                output.push_back(arr[i]);
                len++;

                // 堆塞满后,构造最大堆
                if (len == k) {
                    for (j = k / 2; j >= 0; j--) {
                        maxHeapify(output, j);
                    }
                }

            } else {
                // 元素小于堆顶元素,替换掉首元素
                if (arr[i] < output[0]) {
                    output[0] = arr[i];
                    maxHeapify(output, 0);
                }
            }
        }

        return output;
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/longest-palindrome

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

坚持打卡的第10天!

一、题目描述

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。

在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。

注意:

假设字符串的长度不会超过 1010。

示例 1:

  • 输入:"abccccdd"
  • 输出:7
  • 解释:我们可以构造的最长的回文串是"dccaccd", 它的长度是7。

二、题解

回文串的规律:从左往右数和从右往左数的相同位置上的字符是一样的。那么要想得到回文串,必须找到成对出现的字符。

因此只要从左往右遍历所有的字符,统计出每个字符出现的次数,只要字符出现的次数超过两次,它就能成为回文串中的一员。而出现次数只有一次的字符,就只能乖乖放在最中间了,并且所有只出现一次的字符中,只能选一个放到中间。

三、代码

class Solution {
public:
    int longestPalindrome(string s) {
        int stat[52] = { 0 };
        int i, idx, cnt, flag;
        // 统计所有字符串出现的次数
        for (i = 0; i < s.size(); i++) {
            if (s[i] >= 'a' && s[i] <= 'z') {
                stat[s[i] - 'a']++;
            } else {
                stat[s[i] - 'A' + 26]++;
            }
        }

        // flag用来标记是否存在不重复的字符,可以放在最中间
        flag = 0;
        cnt = 0;
        for (i = 0; i < 52; i++) {
            if (stat[i] >= 2)
                cnt += 2 * (stat[i] / 2);

            if (!flag && stat[i] % 2)
                flag = 1;
        }
        return cnt + flag;
    }
};