LeetCode 15. 3Sum

LeetCode 第 15 題: 3 Sum

題目描述

題目解釋:給定一個整數陣列 S,找出裡面 3 個 element 相加為 0 的所有組合。


Step 1: 新增最單純的測試用例,三個 element 相加剛好為 0,test_nums_0_0_0

測試代碼:(含重構 assertion 的部分)

        private static void ShouldBeEqual(int[] nums, List<List<int>> expected)
        {
            var actual = new Solution().ThreeSum(nums);

            expected.ToExpectedObject().ShouldEqual(actual);
        }

        [TestMethod]
        public void test_nums_0_0_0()
        {
            var nums = new int[] { 0, 0, 0 };
            var expected = new List<List<int>>
                {new List<int>() {0, 0, 0}};

            ShouldBeEqual(nums, expected);
        }

生產代碼:

    public class Solution
    {
        public IList<IList<int>> ThreeSum(int[] nums)
        {
            return null;
        }
    }

Step 2: 使用 3 個 for loop 通過測試

生產代碼:

以3層迴圈通過測試用例

Step 3: 新增測試用例,test_nums_m1_m2_0_3

測試用例代表性:陣列有 4 個元素,存在一組唯一解,{-1, -2, 3}。

新增測試代碼:

生產代碼:加入判斷 item1 + item2 + item3 是否為 0,為 0 才符合需求。

生產代碼迭代差異

Step 4: 使用 HashSet<IList<int>> 排除相同 3 個 element 的重複組合解

生產代碼: 改用 HashSet<IList<int>> 取代 原本的 List<IList<int>>

改用 HashSet

自訂的 ListComparer 如下,覆寫 Equals() 強制排序後做比較:

自訂 ListComparer

Step 5: 優化演算法,先排序,加 condition 減少巡覽次數

生產代碼:因為需求是三個數字相加等於 0,所以排序後,只要三個數字相加大於 0 就可以終止巡覽。而前兩個數字相加若大於 0 也可以終止巡覽,因為後面的數字只會更大。

排序,增加 break 判斷式

Step 6: 重構,針對第一層迴圈,若 element 值 > 0 則終止巡覽。

生產代碼:增加第一層迴圈 break 條件。

增加第一層迴圈 break 條件

Step 7: 重構,針對當前數字如果與上一個巡覽數字相同,則略過此次巡覽,因為結果會跟上次一樣。

生產代碼:增加 continue 迴圈的條件。

    public class Solution
    {
        public IList<IList<int>> ThreeSum(int[] oNums)
        {
            var nums = oNums.OrderBy(x => x).ToArray();
            var set = new HashSet<IList<int>>(new ListComparer());

            for (int i = 0; i < nums.Length; i++)
            {
                var item1 = nums[i];

                if (item1 > 0)
                {
                    break;
                }

                if (i > 0 && nums[i] == nums[i - 1])
                {
                    continue;
                }

                for (int j = i + 1; j < nums.Length; j++)
                {
                    var item2 = nums[j];
                    if (item1 + item2 > 0)
                    {
                        break;
                    }

                    if (j > i + 1 && nums[j] == nums[j - 1])
                    {
                        continue;
                    }

                    for (int k = j + 1; k < nums.Length; k++)
                    {
                        var item3 = nums[k];

                        var threeSum = item1 + item2 + item3;
                        if (threeSum > 0)
                        {
                            break;
                        }

                        if (k > j + 1 && nums[k] == nums[k - 1])
                        {
                            continue;
                        }

                        if (threeSum == 0)
                        {
                            set.Add(new List<int> { item1, item2, item3 });
                        }
                    }
                }
            }

            return set.ToList();
        }
    }

Step 8: 重構,把 HashSet 換回 List,因為已經排序+判斷是否與上一次數字相同,不會存在重複的組合解

生產代碼:

改回 List 以避免不必要的判斷

Step 9: 重構,減少第一層迴圈巡覽數量

生產代碼:第一層迴圈只需巡覽到倒數第三個 element,因為最後兩個在第二跟第三個迴圈會拿來用。

調整第一層迴圈長度

Step 10: 重新調整演算法,用 start, end 旗標取代第二、第三層迴圈

生產代碼:第二層迴圈,設定一個 start 旗標由起點的 element index 開始,一個 end 旗標由最後一個 element index 開始,面對面逼近。

如果 num1 + num[start] + num[end] > 0,代表 end 要再小一點。如果小於 0 代表 start 要再大一點。

    public class Solution
    {
        public IList<IList<int>> ThreeSum(int[] oNums)
        {
            var nums = oNums.OrderBy(x => x).ToArray();
            var set = new List<IList<int>>();

            for (int i = 0; i < nums.Length - 2; i++)
            {
                var item1 = nums[i];

                if (item1 > 0)
                {
                    break;
                }

                if (i > 0 && nums[i] == nums[i - 1])
                {
                    continue;
                }

                var start = i + 1;
                var end = nums.Length - 1;
                while (start < end)
                {
                    if (start > i + 1 && nums[start] == nums[start - 1])
                    {
                        start++;
                        continue;
                    }

                    if (end < nums.Length - 1 && nums[end] == nums[end + 1])
                    {
                        end--;
                        continue;
                    }

                    var threeSum = nums[start] + nums[end] + item1;
                    if (threeSum == 0)
                    {
                        set.Add(new List<int>() { item1, nums[start], nums[end] });
                        end--;
                    }
                    else if (threeSum > 0)
                    {
                        end--;
                    }
                    else
                    {
                        start++;
                    }
                }
            }

            return set;
        }
    }

Step 11: 重構,重新命名與加入 break 判斷式,最終版本生產代碼

生產代碼:

    public class Solution
    {
        public IList<IList<int>> ThreeSum(int[] oNums)
        {
            var nums = oNums.OrderBy(x => x).ToArray();
            var set = new List<IList<int>>();

            for (int i = 0; i < nums.Length - 2; i++)
            {
                var a = nums[i];

                if (a > 0)
                {
                    break;
                }

                if (i > 0 && a == nums[i - 1])
                {
                    continue;
                }

                var start = i + 1;
                var end = nums.Length - 1;
                while (start < end)
                {
                    var b = nums[start];
                    if (a + b > 0)
                    {
                        break;
                    }

                    if (start > i + 1 && b == nums[start - 1])
                    {
                        start++;
                        continue;
                    }

                    var c = nums[end];
                    if (end < nums.Length - 1 && c == nums[end + 1])
                    {
                        end--;
                        continue;
                    }

                    var threeSum = a + b + c;
                    if (threeSum == 0)
                    {
                        set.Add(new List<int>() { a, b, c });
                        end--;
                    }
                    else if (threeSum > 0)
                    {
                        end--;
                    }
                    else
                    {
                        start++;
                    }
                }
            }

            return set;
        }
    }

通過 LeetCode 上所有測試用例

通過 LeetCode 所有測試用例

結論

不少的調整過程,最後大半的演算法重寫,但仍可以看到,原先版本的思路與代碼片段仍保留著其精髓。這也是 TDD 比較輕鬆的地方,你可以用最快的速度完成滿足需求的生產代碼,有餘力時重構調整,那怕是整骨型的演算法重寫,你的思路也不需要重來,你前面花力氣寫的測試用例不會白費。

更何況,最大的差異在於「化繁為簡」跟「敏捷式的儘早交付」,沒有 TDD 很容易就想一步登天,邊調邊錯,更糟糕的是調了不知道有錯。有 TDD 就是每跨出一步都是比較輕鬆的,你知道你要走的路有哪一些,而現在走到哪裡。

GitHub Commit History: LeetCode_15_3Sum

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容