分类 数据结构和算法 下的文章

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/split-array-largest-sum

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

一、题目描述

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:

数组长度 n 满足以下条件:1 ≤ n ≤ 1000, 1 ≤ m ≤ min(50, n)

示例:

  • 输入:nums = [7,2,5,10,8], m = 2
  • 输出:18
  • 解释:一共有四种方法将nums分割为2个子数组,其中最好的方式是将其分为[7,2,5] 和 [10,8],因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

二、题目解析

咋一看题目,完全不知道如何下手。唯一能想到的一个办法是动态规划,但是动态规划要怎么规划。。。算了,看题解吧。

看了题解才知道要用贪心+二分,所谓贪心,就是一种策略思想,在满足条件的情况下寻找最优解。找最优解的办法就是二分,而实际上也就是不断通过二分遍历和去试。

假设数组为nums,要分成m个子数组。那么不难看出和肯定是在[max(nums), sum(nums)]的范围的:

  • 当m等于数组的长度n时,每个元素一个子数组,最大和是数组中的最大元素max(nums)
  • 当m等于1时,整个数组就是一个子数组,最大和是所有数组的和sum(nums)

在了解了这个特性后,就可以使用二分不断遍历范围内的和,然后判断和是不是满足数组条件的和。

判断和是否满足条件的办法:当前遍历到的和是sum,从数组开头开始遍历,计算累加和小于等于sum的子数组总量。如果子数组的数量大于m,说明子数组太多了,也就是sum太小了,把遍历和的左边界设置为sum+1。如果子数组的数量小于或者等于m,说明子数组太少了,sum设置的太大,修改右边界为sum-1。

以测试数据为例,[7,2,5,10,8]的和范围在[10, 32]之间,获取最小的最大和的办法:

第一步,取中值mid=21,计算出来符合条件的子数组为:

[7,2,5,10] [8]

子数组m的个数刚好等于2,设置right=mid-1=20,再计算mid=15,符合条件的数组:

[7,2,5] [10] [8]

子数组个数为3,大于要求的2,设置left=mid+1=16,计算mid=(16+20)/2=18,符合条件的数组:

[7,2,5] [10,8]

子数组个数为2,满足题目要求,设置right=mid-1=17,计算mid=(16+17)/2=16,符合条件的数组:

[7,2,5] [10] [8]

子数组个数为3,大于要求的2,设置left=mid+1=17,计算mid=(17+17)/2=17,符合条件的数组:

[7,2,5] [10] [8]

子数组个数还是3,大于要求的2,设置left=mid+1=18,此时left大于right(17),遍历结束,返回18。

三、示例代码

3.1 代码

class Solution {
public:
    int splitArray(vector<int> &nums, int m) {
        size_t i;
        int count = 0;
        long left = nums[0], right = 0, mid, sum;
        // 确定左边界
        for (i = 0; i < nums.size(); i++) {
            right += nums[i];
            left = left < nums[i] ? nums[i] : left;
        }

        while (left <= right) {
            mid = (left + right) / 2;
            sum = 0;
            count = 1; // count要初始化为1
            // 遍历所有的和
            for (i = 0; i < nums.size(); i++) {
                // 和超出了,重新计算下一个子数组
                if (sum + nums[i] > mid) {
                    sum = nums[i];
                    count ++;
                } else {
                    sum += nums[i];
                }
            }
            if (count > m) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return (int)left;
    }
};

3.2 为什么count初始化为1

在评论区有看到一个问题是问为什么count要初始化为1,回复是说默认情况下是整个数组,所以要加1:

对于这个答案,我是不认可的。

count加1的真正原因是因为最后一个子数组没有计算到count里面去,因为最后一个子数组走不到count++循环就退出了。

cnt=0   cnt=1    循环退出
 ↓          ↓         ↓
[]    [7,2,5]   [10, 8]

当循环退出的时候count来不及加1就退出了,正确的做法就是在循环结束后再自加一次,初始化为1的目的就是相当于提前加了这个1。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/next-greater-element-iii

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

一、题目描述

给定一个32位正整数 n,你需要找到最小的32位整数,其与 n 中存在的位数完全相同,并且其值大于n。如果不存在这样的32位整数,则返回-1。

示例 1:

  • 输入: 12
  • 输出: 21

示例 2:

  • 输入: 21
  • 输出: -1

二、题目解析

题目意思梳理:给定一个数字n(假设是523486,六位数),用同样的数字组成(5、2、3、4、8、6)组合成另外一个六位数,要求这个新组成的数字是所有组合中第一个大于原数字的。注意n是32位的正整数,如果不存在的话返回-1。

看完题目可能并没有好的思路,可以先考虑一下,什么场景下会不存在这个数字。例如:

  • 99999
  • 98765
  • 33322

可以看出的一个规律是:如果数字都是降序排列的话,是找不到一个比它大数字的。为什么呢?如果一个数,低位的数字比高位的数字都小,那么不可能存在一种办法可以把低位较小的数字放到高位去反而比原来的数字还要大的。

只存在一种情况就是把低位较大的数字和高位较小的数字对换才会比原来数字大。那也就说明,如若要找到一个比原来数大的组合,这n的内部一定存在一个数,比它右边的数字要小。进而就说明,从低位往高位,一定存在降序数列。例如:

  • 95876 ==> 96578
  • 82431 ==> 83124

在知晓了这个情况之后,如何取得最小值呢?

以95876为例,从低位往高位数,第一个降序的位置是[..., 5, 8, ...],如果在这里用一个低位的大于5的数字替换掉它就可以得到一个大于95876的数字了。那么如何替换才能最小?肯定是把右边大于5的最小的数字替换过来才最小,也就是用6来替换了,替换后得到96875。

96875是最小的吗?不是,96578才是最小。96875和96578有什么区别?最后三位数字排序不一样,一个升序排列,一个降序排列。降序排列数字由大到小,排列出来肯定是大于升序序列的。

所以能总结出来的操作是:

  1. 从低位往高位寻找到一个降序序列[i, i+1],记录下i的位置。如果不存在这个序列,说明不存在符合条件的数字。
  2. 如果找到了i,从i的右边开始,找到一个最小的大于i的数字,替换它。
  3. 最后把i右边的数据反转,得到一个升序序列。

除此之外,还有一个要注意的问题是要考虑数据溢出,可能原始数据没有溢出,但是比它大的那个数字就溢出了。例如1999999999,比它大的最小的数字是9199999999,这个数字溢出了。

三、代码实现

3.1 考虑数字溢出

leetcode第七题“正数反转”就已经遇到过这个问题了。假设抓换后的当前数字为x,新计算出来的x = x * 10 + y。当x > INT_MAX / 10 或者x == INT_MAX / 10 && y > 7的时候,计算x会溢出,具体的可以参考:leetcode7-整数反转

3.2 数字反转

需要实现一个函数,反转固定区间内的数字:

/*
 * 反转数组,左闭右开区间[left, right)。
 * @left 左下标,包含当前值
 * @right 右下标,不包含当前值
 */
void reverse(vector<int> &v, size_t left, size_t right) {
    int tmp;
    size_t idx;
    for (idx = left; idx < (left + right) / 2; idx++) {
        tmp = v[idx];
        v[idx] = v[right - 1 - (idx - left)];
        v[right - 1 - (idx - left)] = tmp;
    }
}

3.3 交换函数

void my_swap(int &a, int &b) {
    int c = a;
    a = b;
    b = c;
}

3.4 实现

    int nextGreaterElement(int n) {
        int x = n, output = 0;
        int i, dec_idx = -1, y;
        vector<int> v;

        // 按位放到数组中去,并按照逻辑顺序把数组反转
        while (x) {
            v.push_back(x % 10);
            x /= 10;
        }
        reverse(v, 0, v.size());

        // 找到第一个下降的元素下标
        for (i = v.size() - 1; i > 0; i--) {
            if (v[i] > v[i - 1]) {
                dec_idx = i - 1;
                break;
            }
        }
        // 没有找到
        if (i == 0)
            return -1;

        // y是最小的大于v[dec_idx]的元素的下标,它不可能是-1,一定能找到
        y = dec_idx + 1;
        for (i = dec_idx + 2; i < v.size(); i++) {
            if (v[i] > v[dec_idx] && v[i] <= v[y]) {
                y = i;
            }
        }

        // 交换
        my_swap(v[dec_idx], v[y]);
        // 反转右边的数字
        reverse(v, dec_idx + 1, v.size());

        // 把数组转换成单个数字
        for (i = 0; i < v.size(); i++) {
            // 处理数据溢出
            if (output > INT_MAX / 10 || (output == INT_MAX && v[i] > (INT_MAX % 10)))
                return -1;
            output = output * 10 + v[i];
        }

        return output;
    }

在题解中看到的一个可以优化的点是:找到i后,从i开始往右边找到一个最小的大于它的数字,可以使用二分查找来完成。不过题目限制的是32位整数,总共不超过10位,个人认为优化不明显。

实际测试中,使用这个二分法查找,执行耗时更短。

四、单元测试

测试案例的后面几个都是leetcode提交中出错的案例:

TEST(nextGreaterElement, leetcode) {
    Solution s;
    EXPECT_EQ(s.nextGreaterElement(877645132), 877645213);
    EXPECT_EQ(s.nextGreaterElement(12), 21);
    EXPECT_EQ(s.nextGreaterElement(21), -1);
    EXPECT_EQ(s.nextGreaterElement(12222333), 12223233);
    EXPECT_EQ(s.nextGreaterElement(12443322), 13222344);
    EXPECT_EQ(s.nextGreaterElement(1999999999), -1);
}

一、二分查找

template<typename T>
int bin_search(const T data[], int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] > target) {
            right = mid - 1;
        } else if (data[mid] < target) {
            left = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

二、查找最后一个小于n的值

// 查找第一个大于目标的元素
template<typename T>
int bin_search_first_greater(const T data[], int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] > target) {
            if (mid == left || data[mid - 1] < target)
                return mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return -1;
}

三、查找第一个大于n的值

template<typename T>
int bin_search_last_less(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            if (mid == right || data[mid + 1] > target)
                return mid;
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

四、查找第一个等于n的值

template<typename T>
int bin_search_first_equal(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            left = mid + 1;
        } else if (data[mid] > target) {
            right = mid - 1;
        } else {
            if (mid == left || data[mid - 1] < target)
                return mid;
            right = mid - 1;
        }
    }
    return -1;
}

五、查找最后一个等于n的值

template<typename T>
int bin_search_last_equal(const T *data, int left, int right, T target) {
    int mid;
    while (left <= right) {
        mid = (left + right) / 2;
        if (data[mid] < target) {
            left = mid + 1;
        } else if (data[mid] > target) {
            right = mid - 1;
        } else {
            if (mid == right || data[mid + 1] > target)
                return mid;
            left = mid + 1;
        }
    }
    return -1;
}

六、单元测试

6.1 TestFixtures

class bin_search_test : public ::testing::Test {
protected:
    void SetUp() override {
        //                              0    1   2  3  4  5  6  7   8    9
        arr1 = new int[test_ele_count]{-23, -5, -2, 3, 8, 8, 9, 11, 54, 100};
        arr2 = new int[test_ele_count]{-1, 1, 3, 5, 5, 5, 6, 6, 6, 6};
    }

    void TearDown() override {
        delete arr1;
        delete arr2;
    }

    int *arr1;
    int *arr2;
};

6.2 测试案例

// 测试案例:二分搜索
TEST_F(bin_search_test, bin_search) {
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 9), 6);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 54), 8);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -23), 0);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 100), 9);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -1), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, -3), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 7), -1);
    EXPECT_EQ(bin_search(arr1, 0, test_ele_count - 1, 60), -1);
}

// 测试案例:第一个大于目标的值
TEST_F(bin_search_test, bin_search_first_greater) {
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 0), 3);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, -5), 2);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 100), -1);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, -24), 0);
    EXPECT_EQ(bin_search_first_greater(arr1, 0, test_ele_count - 1, 15), 8);
}

// 测试案例:最后一个小于目标
TEST_F(bin_search_test, bin_search_last_less) {
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, 20), 7);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, 90), 8);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, -1), 2);
    EXPECT_EQ(bin_search_last_less(arr1, 0, test_ele_count - 1, -40), -1);
}

// 测试案例:第一个等于目标
TEST_F(bin_search_test, bin_search_first_equal) {
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 5), 3);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 6), 6);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 1), 1);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 12), -1);
}

// 测试案例:最后一个等于目标
TEST_F(bin_search_test, bin_search_last_equal) {
    EXPECT_EQ(bin_search_last_equal(arr2, 0, test_ele_count - 1, 5), 5);
    EXPECT_EQ(bin_search_last_equal(arr2, 0, test_ele_count - 1, 6), 9);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 1), 1);
    EXPECT_EQ(bin_search_first_equal(arr2, 0, test_ele_count - 1, 12), -1);
}

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/reverse-string

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

一、题目描述

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组char[]的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是ASCII码表中的可打印字符。

示例 1:

  • 输入:["h","e","l","l","o"]
  • 输出:["o","l","l","e","h"]

示例 2:

  • 输入:["H","a","n","n","a","h"]
  • 输出:["h","a","n","n","a","H"]

二、解析

从字符串开头遍历到中间位置,同时反转每个对应位置上的元素。很基础的题目,没有坑。

三、代码

class Solution {
public:
    void reverseString(vector<char>& s) {
        size_t i, len;
        char ch;

        len = s.size();
        for (i = 0; i < len / 2; i++) {
            ch = s[i];
            s[i] = s[len - 1 - i];
            s[len - 1 - i] = ch;
        }
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/reverse-integer

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

一、题目描述

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。

示例:

  • 输入: 123
  • 输出: 321

示例 2:

  • 输入: -123
  • 输出: -321

示例 3:

  • 输入: 120
  • 输出: 21

二、题解

题目很简单,基本的思路就是对n循环取模,然后乘10累加。但是问题的关键点不是在这里,关键点是数据溢出。假设传入数据比INT_MAX小,但是反转后比INT_MAX大,那就会产生溢出。

INT_MAX = 2147483647, INT_MIN = -2147483648;

假设反转后的值是output,被反转数字模10后得到y,那么output应该被设置为output * 10 + y

此时output溢出的条件:

  1. output > INT_MAX / 10或者output = INT_MAX / 10 && y > 7
  2. output < INT_MIN / 10或者output = INT_MIN / 10 && y < -8

以第一点为例,假设此时output大于最大值除以10,那么它乘以10后肯定会大于最大值,也就会溢出。如果它等于最大值除10,那么只要y大于最大值取模,也会溢出。

三、代码

class Solution {
public:
    int reverse(int x) {
        int output = 0, y;
        while (x) {
            y = x % 10;
            if (output > INT_MAX / 10 || (output == INT_MAX / 10 && x > 7)) return 0;
            if (output < INT_MIN / 10 || (output == INT_MIN / 10 && x < -8)) return 0;
            output = output * 10 + y % 10;
            x /= 10;
        }
        return output;
    }
};

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/copy-list-with-random-pointer

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

一、题目描述

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深拷贝。

我们用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

示例 1:

  • 输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
  • 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

  • 输入:head = [[1,1],[2,1]]
  • 输出:[[1,1],[2,1]]

提示:

  • -10000 <= Node.val <= 10000。
  • Node.random 为空(null)或指向链表中的节点。
  • 节点数目不超过 1000 。

二、题目解析

《剑指offer》原题,详细解析《剑指offer》面试题35:复杂链表的复制

三、代码

class Solution {
private:
    /*
     * 克隆所有的节点,并串到原始节点中去
     * @head 链表的头结点
     */
    void clone_all_nodes(Node *head) {
        Node *node, *clone;
        node = head;
        while (node) {
            // 拷贝节点
            clone = new Node(node->val);
            clone->next = node->next;
            clone->random = node->random;
            // 修改p节点的下一个节点指向新创建的节点
            node->next = clone;
            // 移动p指针到下一个节点
            node = clone->next;
        }
    }

    /*
     * 修正所有克隆节点的随机指针的地址
     * @head 链表的头结点
     */
    void fix_random_nodes(Node *head) {
        // node/q节点分别表示原始节点和被克隆的节点
        Node *node, *clone;
        node = head;
        while (node) {
            clone = node->next;
            // 修正q节点的random指针,注意random可能为null
            if (clone->random)
                clone->random = clone->random->next;
            node = clone->next;
        }
    }

    Node *reconnect_nodes(Node *head) {
        Node *node, *clone;
        Node *new_head = head->next;

        node = head;
        while (node) {
            clone = node->next;
            // 备份下一个节点的指针
            node->next = clone->next;
            // 连接到新克隆的节点,注意next可能为null
            if (clone->next) {
                clone->next = clone->next->next;
            }
            node = node->next;
        }
        return new_head;
    }

public:
    Node *copyRandomList(Node *head) {
        Node *new_head;
        if (head == nullptr) {
            return nullptr;
        }
        clone_all_nodes(head);
        fix_random_nodes(head);
        new_head = reconnect_nodes(head);
        return new_head;
    }
};

一、题目描述

请实现函数complex_list_node *clone_list(complex_list_node *head),复制一个复杂链表。在复杂链表中,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的随机节点或nullptr。节点的C++定义如下:

struct complex_list_node {
    int val;
    struct complex_list_node *next;
    struct complex_list_node *random;
};

如下是一个复杂链表示例,其中虚线是random指针:

二、思路

复制过程可以分为三步,第一步,复制链表中的所有节点并插入到对应节点后,random指针和原节点指针保持一致:

第二步,修改所有复制节点的random指针,修改成被复制出来的节点:

第三步,把所有新复制出来的节点从原始链表中移除:

三、代码实现

链表节点定义:

// 链表节点定义
struct complex_list_node {
    int val;
    struct complex_list_node *next; // 下一个节点
    struct complex_list_node *random; // 随机节点
};

第一步克隆所有节点函数:

/*
 * 克隆所有的节点,并串到原始节点中去
 * @head 链表的头结点
 */
static void clone_all_nodes(complex_list_node *head) {
    complex_list_node *node, *clone;
    node = head;
    while (node) {
        // 拷贝节点
        clone = new complex_list_node{node->val, node->next, node->random};
        // 修改p节点的下一个节点指向新创建的节点
        node->next = clone;
        // 移动p指针到下一个节点
        node = clone->next;
    }
}

第二步修改random指针指向:

/*
 * 修正所有克隆节点的随机指针的地址
 * @head 链表的头结点
 */
static void fix_random_nodes(complex_list_node *head) {
    // node/q节点分别表示原始节点和被克隆的节点
    complex_list_node *node, *clone;
    node = head;
    while (node) {
        clone = node->next;
        // 修正q节点的random指针,注意random可能为null
        if (clone->random)
            clone->random = clone->random->next;
        node = clone->next;
    }
}

第三步分离原始链表和新克隆链表:

/*
 * 修改新复制节点的指向,重新连接所有节点
 * @head 头结点指针
 * @return 新复制的链表头结点
 */
static complex_list_node *reconnect_nodes(complex_list_node *head) {
    complex_list_node *node, *clone;
    complex_list_node *new_head = head->next;

    node = head;
    while (node) {
        clone = node->next;
        // 备份下一个节点的指针
        node->next = clone->next;
        // 连接到新克隆的节点,注意next可能为null
        if (clone->next) {
            clone->next = clone->next->next;
        }
        node = node->next;
    }
    return new_head;
}

整合起来的复杂链表拷贝函数:

complex_list_node *clone_complex_list(complex_list_node *head) {
    if (head == nullptr)
        return nullptr;
    clone_all_nodes(head);
    fix_random_nodes(head);
    return reconnect_nodes(head);
}

注意:这个函数里面一定要判断head是否为空的情况,前面三个函数因为是内部函数所以没有在入口判断head为空的情况!

四、单元测试

写代码容易,写单元测试麻烦,一个单元测试写了一小时。。。

4.1 链表创建函数

做单元测试,必不可少的一个测试函数是链表创建函数,因为要测试链表,首先要先创建链表:

/*
 * 链表创建函数
 * @val 数据数组
 * @random 随机指针
 * @n 数组数量
 * @return 返回链表头
 */
static complex_list_node *list_creator(int data[], int random[], int n) {
    complex_list_node *head, **nodes, *p;
    int i;
    nodes = new p_complex_list_node[n];

    // 创建所有的链表节点
    for (i = 0; i < n; i++) {
        p = new complex_list_node{data[i], nullptr, nullptr};
        nodes[i] = p;
    }

    // 连接所有的next和random指针
    for (i = 0; i < n - 1; i++) {
        nodes[i]->next = nodes[i + 1];
        if (random[i] == -1) {
            nodes[i]->random = nullptr;
        } else {
            nodes[i]->random = nodes[random[i]];
        }
    }
    nodes[i]->random = nodes[random[i]];

    p = nodes[0];
    delete nodes;

    return p;
}

测试这个函数的正确性:

// 测试创建链表的功能是不是OK
TEST(complex_list_node, list_creator) {
    // 数据数组
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 随机指针数组
    int random[10] = {2, 3, 6, -1, 3, 1, 0, 9, 5, 3};
    int i;
    complex_list_node *head, *p, *r, *n;
    // 创建链表
    head = list_creator(arr, random, 10);
    p = head;
    for (i = 0; i < 10 && p; i++, p = p->next) {
        r = p->random;
        EXPECT_EQ(p->val, arr[i]);
        if (random[i] == -1) {
            EXPECT_EQ(r, nullptr);
        } else {
            EXPECT_EQ(r->val, arr[random[i]]);
        }
    }
}

4.1 测试功能

测试功能点:

  1. 空链表测试
  2. 非空链表测试
  3. 链表克隆后是否会影响原始链表

leetcode上也有这个一模一样的题目,下面的测试数据是基于leetcode来写的。

https://leetcode-cn.com/problems/copy-list-with-random-pointer/submissions/

TestFixtures定义:

const int leetcode_demo1_cnt = 5;
class leetcode_test : public ::testing::Test {
protected:
    void SetUp() override {
        // 初始化list1
        int arr[leetcode_demo1_cnt] = {7, 13, 11, 10, 1};
        int random[leetcode_demo1_cnt] = {-1, 0, 4, 2, 0};
        list1 = list_creator(arr, random, leetcode_demo1_cnt);
        // list2为空
        list2 = nullptr;
    }

    void TearDown() override {
        complex_list_node *p = list1, *next;
        while (p) {
            next = p->next;
            delete p;
            p = next;
        }
    }

    complex_list_node *list1;
    complex_list_node *list2;
};

测试克隆链表案例:

TEST_F(leetcode_test, clone_nodes) {
    int i;
    complex_list_node *clone, *p, *q;
    clone = clone_complex_list(list1);

    p = list1;
    q = clone;
    // 遍历所有节点,所有节点的内容和原节点一样,并且克隆出来的节点并不是原链表上面的指针
    for (i = 0; i < leetcode_demo1_cnt && p && q; i++, p = p->next, q = q->next) {
        // 两个节点不相等
        EXPECT_NE(p, q);
        // 节点的值都一样
        EXPECT_EQ(p->val, q->val);
        if (p->next) {
            EXPECT_TRUE(q->next);
            EXPECT_NE(p->next, q->next);
        }
        if (p->random) {
            EXPECT_TRUE(q->random);
            EXPECT_NE(q->random, p->random);
        }
    }
    // 都遍历的链表结尾了
    EXPECT_FALSE(p);
    EXPECT_FALSE(q);
    EXPECT_EQ(i, leetcode_demo1_cnt);
}

测试空链表复制:

TEST_F(leetcode_test, empty_list) {
    complex_list_node *clone;
    clone = clone_complex_list(list2);
    EXPECT_FALSE(clone);
}

测试是否修改了原链表:

TEST_F(leetcode_test, origin_list_not_modify) {
    int i = 0;
    complex_list_node *bak[leetcode_demo1_cnt], *p, *clone;
    // 备份原始链表的所有next指针
    p = list1;
    while (p) {
        bak[i++] = p->next;
        p = p->next;
    }

    clone = clone_complex_list(list1);

    // 和克隆前的next指针对比,确认没有影响到原始指针
    p = list1;
    i = 0;
    while (p) {
        EXPECT_EQ(bak[i++], p->next);
        p = p->next;
    }
}

测试结果:

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list

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

一、题目描述

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

例如:给定一个链表: 1->2->3->4->5, 和 n = 2。当删除了倒数第二个节点后,链表变为 1->2->3->5。

说明:

给定的 n 保证是有效的。

进阶:你能尝试使用一趟扫描实现吗?

二、题解

使用快慢指针,快指针先走n步,满指针再走,当快指针为空的时候满指针就是倒数第n个节点。

《剑指offer》面试题22:链表中的倒数第k个节点类似,知识剑指offer中是返回倒数第n个节点,这道题目是删除倒数第n各节点,多了个删除操作。

三、代码

class Solution {
public:
    ListNode *removeNthFromEnd(ListNode *head, int n) {
        ListNode *prev, *fast, *slow;
        prev = nullptr;
        fast = head;
        slow = head;

        // 快指针先走n步
        while (fast && n--)
            fast = fast->next;

        // n大于链表长度,直接返回head
        if (!fast && n > 0)
            return head;

        // 遍历快慢指针,并备份慢指针
        while (fast != nullptr) {
            prev = slow;
            fast = fast->next;
            slow = slow->next;
        }

        // 删除节点,注意删除的节点可能是链表收尾节点
        if (prev) {
            // 如果删除的是最后一个节点,slow为空
            prev->next = slow ? slow->next : nullptr;
            return head;
        } else {
            // 删除的首节点,返回首节点的下一个节点
            return head->next;
        }
    }
};

一、题目

输入一个链表,输出该链表中倒数第k个节点,为了符合大多数人的习惯,k的序号从1开始,即链表的尾结点是倒数第一个节点。

例如,如下链表中的倒数第3个节点是3

二、解题思路

使用快慢指针,快指针先走n步,然后快慢指针同时走,直到快指针为null,慢指针就是倒数第n个节点。

以上面的链表为例,初始时,两个指针都指向链表开头:

fast指针先往前走三步:

此时,fast和slow同时往前走,直到fast为空:

此时slow所在的位置刚好就是倒数第三个节点。

三、代码

代码中需要注意的问题:

  1. 链表可能为空,避免空指针访问。
  2. 链表长度可能不够n,遍历fast的时候注意判断非空,并且外部应该判断出这一点。
list_node *find_kth_to_tail(list_node *head, int n) {
    list_node *slow, *fast;
    
    // [1] 注意链表为空的情况
    if (head == nullptr)
        return nullptr;

    fast = head;
    slow = head;

    // [2] fast指针先走n步
    while (fast && n--) {
        fast = fast->next;
    }

    // [3] 整个链表长度不够n
    if (!fast && n > 0)
        return nullptr;

    while (fast) {
        fast = fast->next;
        slow = slow->next;
    }

    return slow;
}

四、单元测试

单元测试主要覆盖以下几点:

  1. 链表为空的场景。
  2. 链表不为空,获取倒数第1个节点。
  3. 链表不为空,获取倒数第n个节点,n等于链表长度。
  4. 链表不为空,获取倒数第n个节点,n大于链表长度。

TestFixtures定义了三个链表,第一个链表有5个元素,第二个链表为空,第三个链表有1个元素。

class list_test : public ::testing::Test {
protected:
    void SetUp() override {
        int i, arr[] = {2, 3, 4, 5};
        // 初始化链表1
        list_node *p;
        p = new list_node{1, nullptr};
        list1 = p;
        for (i = 0; i < 4; i++) {
            p->next = new list_node{arr[i], nullptr};
            p = p->next;
        }
        // 初始化链表2
        list2 = nullptr;
        // 初始化链表3
        list3 = new list_node{1, nullptr};
    }

    // 释放链表节点
    void TearDown() override {
        list_node *p, *next;
        p = list1;
        while (p) {
            next = p->next;
            delete p;
            p = next;
        }
        delete list3;
        list3 = nullptr;
    }

    // 初始化三个链表,第一个链表有5个节点,第二个链表为空,第三个链表只有一个节点
    list_node *list1;
    list_node *list2;
    list_node *list3;
};

测试案例一:获取倒数第n个节点,n分别大于、小于和等于链表长度。

// 案例:获取倒数第n个节点,n小于链表长度
TEST_F(list_test, multi_node) {
    list_node *p;
    // 倒数第一个节点
    p = find_kth_to_tail(list1, 1);
    EXPECT_EQ(p->data, 5);
    // 倒数第二个节点
    p = find_kth_to_tail(list1, 2);
    EXPECT_EQ(p->data, 4);
    // 倒数第三个节点
    p = find_kth_to_tail(list1, 3);
    EXPECT_EQ(p->data, 3);
    // 倒数第四个节点
    p = find_kth_to_tail(list1, 4);
    EXPECT_EQ(p->data, 2);
    // 倒数第五个节点
    p = find_kth_to_tail(list1, 5);
    EXPECT_EQ(p->data, 1);
    // 倒数第六个节点 --> 空
    p = find_kth_to_tail(list1, 6);
    EXPECT_FALSE(p);
}

测试案例二:测试空链表。

// 案例:空链表测试
TEST_F(list_test, no_node) {
    list_node *p;
    p = find_kth_to_tail(list2, 1);
    EXPECT_FALSE(p);
    p = find_kth_to_tail(list2, 2);
    EXPECT_FALSE(p);
}

测试案例三:只有一个节点的链表测试。

// 案例:只有一个节点的链表测试
TEST_F(list_test, one_node) {
    list_node *p;
    // 倒数第一个节点,不为空
    p = find_kth_to_tail(list3, 1);
    EXPECT_EQ(p->data, 1);
    // 倒数第二个节点,空
    p = find_kth_to_tail(list3, 2);
    EXPECT_FALSE(p);
    // 倒数第三个节点,空
    p = find_kth_to_tail(list3, 3);
    EXPECT_FALSE(p);
}

测试结果:

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/range-sum-query-immutable

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

一、题目描述

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()。

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

  1. 你可以假设数组不可变。
  2. 会多次调用 sumRange 方法。

二、题解

题目很迷幻,一眼看上去可能会误解成就是求指定区间的和,如果只是求指定区间内的和,除了遍历就没有其他办法了。而实际上整道题目的重点就是会多次调用,也就是说同一份数据,会有多个测试案例。

很容易能猜出最容易出现问题的地方就是超时了,因为实际的问题很简单,就是求和,求和只能遍历,不可能就这么简单每次就遍历求和。因此考虑的重点就在于如何在多次调用中减少时间占用。

减少计算时间的办法:缓存数据,即缓存下来部分数据的和,再根据缓存的和来计算特定范围的和。

以测试数据为例,以sum[i]表示下标i以前(不包括i)的所有数据之和,那么sum数组的值为:

0, -2, -2, 1, -4, -2, -3

sumRange(0, 2)就相当于计算sum[2 + 1] - sum[0] ,值为1。

sumRange(2, 5)就相当于计算sum[5 + 1] - sum[2],值为-2。

在这种情况下能总结出来的规律是:sumRange(i, j) = sum[j + 1] - sum[i]。

三、代码

逻辑很简单,但关键点是:sum[i]表示的是i以前所有元素的和,所以要给sum[0]赋值上一个0值。

class NumArray {
private:
    vector<int> sum;
public:
    NumArray(vector<int>& nums) {
        // 构造函数中初始化sum数组
        size_t i;
        // 给sum[0]附上一个0值
        sum.push_back(0);
        for (i = 0; i< nums.size(); i++) {
            sum.push_back(sum[i] + nums[i]);
        }
    }

    int sumRange(int i, int j) {
        return sum[j + 1] - sum[i];
    }
};

优化

可以看到,时间消耗还是很大的,排名才击败了38%。代码中主要的耗时是vector数组的扩容导致的,因为够早的时候需要频繁push_back导致数组扩容和内存拷贝,比较费时。可以把vector改成数组形式,手动分配内存空间,减少vector频繁扩容。

class NumArray {
private:
    int *sum;
public:
    NumArray(vector<int>& nums) {
        size_t i;
        sum = new int[nums.size() + 1];
        sum[0] = 0;
        for (i = 0; i< nums.size(); i++) {
            sum[i + 1] = sum[i] + nums[i];
        }
    }

    int sumRange(int i, int j) {
        return sum[j + 1] - sum[i];
    }
};

重新提交后,时间消耗击败了86%的用户。