LeetCode筆記-三數之和等於0

dust1發表於2018-10-03

今天刷leetcode的時候做到的題目:

三數之和: 給定一個包含 n 個整數的陣列 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 > a + b + c = 0 ?找出所有滿足條件且不重複的三元組。

注意:答案中不可以包含重複的三元組。

例如, 給定陣列 nums = [-1, 0, 1, 2, -1, -4],

滿足要求的三元組集合為: [ [-1, 0, 1], [-1, -1, 2] ]

這個問題我採用

先排序,然後遍歷的時候定一個基值兩個左右移動控制和大小的指標

這種做法,但是讓我困擾的是如何不重複。 最開始的思路是

定義一個int[],大小為給定資料nums裡面最大值max和最小值min的差值 + 1,假設三數分別是a, b, c。當三數之和等於0時,根據

(int i) -> i < 0 : Math.abs(i) + max : i;
複製程式碼

來生成陣列對應的下標x, y, z。如果根據下標從陣列中獲取的三個值之和不等於3,則這個組合沒有出現過。然後將三個下標對應的值賦值為1。

但是這裡有問題了,當引數為int[] nums = {-4, -2, -2, -2, 0, 1, 2, 2, 2, 3, 3, 4, 4, 6, 6}時,前幾個組合分別是[-4, -2, 6], [-4, 0, 4], [-4, 1, 3], [-4, 2, 2]。當下一個組合[-2, -2, 4]時對應的下標為8,8,4。而此時陣列上8和4下標的值已經是1了,所以無法存入最終結果。

發現這個問題之後對他進行改造。

新定義存有效值的String[],它的大小等於nums的大小。在定義一個和它對應的有效下標indexP,每當前面儲存座標的int[]三數之和不等於3的時候將三個值轉成String存入陣列中並且indexP++。如果等於3,則遍歷該String[],如果不存在該組合則加入最終組合,並將組合相連存入String[]

改造之後倒是通過了。最終程式碼如下

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        List<List<Integer>> result = new ArrayList<>();
        if (n < 3) {
            return result;
        }
        int[] data = new int[n];
        for (int i = 0; i < n; i++) {
            data[i] = nums[i];
        }
        Arrays.sort(data);
        
        int min = data[0];
        int max = data[n - 1];
        int[] index = new int[Math.abs(max - min) + 1];
        String[] index2 = new String[n];
        int indexP = 0;

        for (int i = 0; i < n; i++) {
            int a = data[i] < 0 ? Math.abs(data[i]) + max : data[i];
            int j = i + 1;
            int k = n - 1;
            while (j < k) {
                int sum = data[i] + data[j] + data[k];
                if (sum == 0) {
                    int b = data[j] < 0 ? Math.abs(data[j]) + max : data[j];
                    int c = data[k] < 0 ? Math.abs(data[k]) + max : data[k];
                    StringBuilder sb = new StringBuilder();
                    sb.append(a).append(b).append(c);
                    String p = sb.toString();
                    if ((index[a] + index[b] + index[c]) != 3) {
                        List<Integer> list = Arrays.asList(data[i], data[j], data[k]);
                        result.add(list);
                        index[a] = 1;
                        index[b] = 1;
                        index[c] = 1;
                        index2[indexP] = p;
                        indexP++;
                    } else {
                        if (!check(index2, p)) {
                            List<Integer> list = Arrays.asList(data[i], data[j], data[k]);
                            result.add(list);
                            index2[indexP] = p;
                            indexP++;
                        }
                    }
                    k--;
                    j++;
                } else if (sum > 0) {
                    k--;
                } else {
                    j++;
                }
            }
            indexP = 0;
        }
        return result;
    }

    static boolean check (String[] strs, String p) {
        int n = strs.length;
        int i = 0;
        while (strs[i] != null && i < n) {
            if (strs[i].equals(p)) {
                return true;
            }
            i++;
        }
        return false;
    }

}
複製程式碼

此時雖然通過了,但是執行時間十分慢,需要O(n^2) ~ O(n^2 * n!),如果忽略最開始對傳入引數的copy的話.(關於這點我在學習java函數語言程式設計的時候要求方法的副作用盡量少或者沒有,所以強迫式地copy了。)

該文章作為我自己的筆記記錄大佬們對於該題的最快解法。提升姿勢水平。 在leetcode上最快的程式碼如下:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
      if (nums.length < 3)
        return Collections.emptyList();
      List<List<Integer>> res = new ArrayList<>();
      int minValue = Integer.MAX_VALUE;
      int maxValue = Integer.MIN_VALUE;
      int negSize = 0;
      int posSize = 0;
      int zeroSize = 0;
      for (int v : nums) {
        if (v < minValue)
          minValue = v;
        if (v > maxValue)
          maxValue = v;
        if (v > 0)
          posSize++;
        else if (v < 0)
          negSize++;
        else
          zeroSize++;
      }
      if (zeroSize >= 3)
        res.add(Arrays.asList(0, 0, 0));
      if (negSize == 0 || posSize == 0)
        return res;
      if (minValue * 2 + maxValue > 0)
        maxValue = -minValue * 2;
      else if (maxValue * 2 + minValue < 0)
        minValue = -maxValue * 2;

      int[] map = new int[maxValue - minValue + 1];
      int[] negs = new int[negSize];
      int[] poses = new int[posSize];
      negSize = 0;
      posSize = 0;
      for (int v : nums) {
        if (v >= minValue && v <= maxValue) {
          if (map[v - minValue]++ == 0) {
            if (v > 0)
              poses[posSize++] = v;
            else if (v < 0)
              negs[negSize++] = v;
          }
        }
      }
      Arrays.sort(poses, 0, posSize);
      Arrays.sort(negs, 0, negSize);
      int basej = 0;
      for (int i = negSize - 1; i >= 0; i--) {
        int nv = negs[i];
        int minp = (-nv) >>> 1;
        while (basej < posSize && poses[basej] < minp)
          basej++;
        for (int j = basej; j < posSize; j++) {
          int pv = poses[j];
          int cv = 0 - nv - pv;
          if (cv >= nv && cv <= pv) {
            if (cv == nv) {
              if (map[nv - minValue] > 1)
                res.add(Arrays.asList(nv, nv, pv));
            } else if (cv == pv) {
              if (map[pv - minValue] > 1)
                res.add(Arrays.asList(nv, pv, pv));
            } else {
              if (map[cv - minValue] > 0)
                res.add(Arrays.asList(nv, cv, pv));
            }
          } else if (cv < nv)
            break;
        }
      }
      return res;
    }
}
複製程式碼

對於大佬的程式碼我是debug進行分析。

傳入引數nums = [-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6]
使用工具vscode

首先是定義

      int minValue = Integer.MAX_VALUE;
      int maxValue = Integer.MIN_VALUE;
      int negSize = 0;
      int posSize = 0;
      int zeroSize = 0;
複製程式碼

對於計算方面他定義了五個變數,分別是最大值、最小值、大於0的元素數目、小於0的元素數目、等於0的元素數目。

然後是第一個遍歷

for (int v : nums) {
    if (v < minValue)
        minValue = v;
    if (v > maxValue)
        maxValue = v;
    if (v > 0)
        posSize++;
    else if (v < 0)
        negSize++;
    else
        zeroSize++;
}
複製程式碼

這個遍歷是為了對上述五個變數進行賦值。此時各個值為:

LeetCode筆記-三數之和等於0

接著是if判斷

if (zeroSize >= 3)
    res.add(Arrays.asList(0, 0, 0));
if (negSize == 0 || posSize == 0)
    return res;
if (minValue * 2 + maxValue > 0)
    maxValue = -minValue * 2;
else if (maxValue * 2 + minValue < 0)
    minValue = -maxValue * 2;
複製程式碼

如果nums中0元素大於等於3個,則直接將[0, 0, 0]加入最終結果中。如果nums中都為0,則直接返回最終結果。如果最小值 * 2與最大值之和大於0則將最大值賦值為最小值的相反數 * 2,如果最大值 * 2與最小值之和小於0則將最小值賦值為最大值的相反數 * 2。這裡的minValue和maxValue的作用我現在還看不出來,因此直接檢視控制檯的值:

LeetCode筆記-三數之和等於0
我們可以看到對於該例各個數沒有變化

int[] map = new int[maxValue - minValue + 1];
int[] negs = new int[negSize];
int[] poses = new int[posSize];
negSize = 0;
posSize = 0;
複製程式碼

這裡定義的map的長度為最大值和最小值的差值 + 1,以及定義兩個int[],長度分別是nums中大於0和小於0的元素的個數,估計是用來儲存對應的元素的,然後置0 negSize和posSize。

LeetCode筆記-三數之和等於0

for (int v : nums) {
    if (v >= minValue && v <= maxValue) {
      if (map[v - minValue]++ == 0) {
        if (v > 0)
          poses[posSize++] = v;
        else if (v < 0)
          negs[negSize++] = v;
      }
    }
  }
複製程式碼

這裡第二次遍歷,取出位於maxValue和minValue之間的元素,並且如果map中下標v - minValue的下一個元素為0,如果v大於0則poses的元素賦值為v然後下標右移。 此時上面定義的negs和poses的作用用來存符合條件的元素,而negSize和posSize的作用變為這兩個陣列的下標指標。 五個變數的值如下:

LeetCode筆記-三數之和等於0

map的值如下:

LeetCode筆記-三數之和等於0

negs的值:

LeetCode筆記-三數之和等於0

poses的值:

LeetCode筆記-三數之和等於0
這下對於上述程式碼的功能就知道了

它負責統計nums中重複元素的個數以及得到nums中所有不重複的值

Arrays.sort(poses, 0, posSize);
Arrays.sort(negs, 0, negSize);
複製程式碼

這裡將不重複元素按從小到大排列

int basej = 0;
for (int i = negSize - 1; i >= 0; i--) {
    int nv = negs[i];
    int minp = (-nv) >>> 1;
    while (basej < posSize && poses[basej] < minp)
      basej++;
    for (int j = basej; j < posSize; j++) {
      int pv = poses[j];
      int cv = 0 - nv - pv;
      if (cv >= nv && cv <= pv) {
        if (cv == nv) {
          if (map[nv - minValue] > 1)
            res.add(Arrays.asList(nv, nv, pv));
        } else if (cv == pv) {
          if (map[pv - minValue] > 1)
            res.add(Arrays.asList(nv, pv, pv));
        } else {
          if (map[cv - minValue] > 0)
            res.add(Arrays.asList(nv, cv, pv));
        }
      } else if (cv < nv)
        break;
    }
}
複製程式碼

最後的遍歷,由於不為0的數之和要等於0,其中一定會有兩個數一個為負數一個為正數。

int basej = 0;
for (int i = negSize - 1; i >= 0; i--) {
    int nv = negs[i];
    int minp = (-nv) >>> 1;
    while (basej < posSize && poses[basej] < minp)
      basej++;
      /* other code */
}
複製程式碼

這裡的遍歷順序是以負數開始遍歷,並且按照從大到小遍歷,獲取到最大的符合(-nv) >>> 1條件的正數下標。

for (int j = basej; j < posSize; j++) {
    int pv = poses[j];
    int cv = 0 - nv - pv;
    /* other code */
}
複製程式碼

這裡的pv是獲取到的符合條件的最小正數,而cv是為了三數之和為對應條件的剩下的值。

if (cv >= nv && cv <= pv) {
  if (cv == nv) {
    if (map[nv - minValue] > 1)
      res.add(Arrays.asList(nv, nv, pv));
  } else if (cv == pv) {
    if (map[pv - minValue] > 1)
      res.add(Arrays.asList(nv, pv, pv));
  } else {
    if (map[cv - minValue] > 0)
      res.add(Arrays.asList(nv, cv, pv));
  }
} else if (cv < nv)
  break;
複製程式碼

如果最後的值小於負數則退出該負數的遍歷進行下一個,因為之後的遍歷可能會用到。

if (cv == nv) {
    if (map[nv - minValue] > 1)
      res.add(Arrays.asList(nv, nv, pv));
  } else if (cv == pv) {
    if (map[pv - minValue] > 1)
      res.add(Arrays.asList(nv, pv, pv));
  } else {
    if (map[cv - minValue] > 0)
      res.add(Arrays.asList(nv, cv, pv));
  }
複製程式碼

最後根據第三個值是否需要和前兩個數的其中一個重複以及對應元素的個數來獲取最終結果。如果不相等則直接獲取最終結果。

總結

大佬程式碼的思路:

分別獲取nums中不重複的正負元素集合以及這些元素對應的個數,從最大的負數開始遍歷,先獲取元素 * 2 小於負數的最大的正數,然後開始遍歷正數。然後根據最終結果計算出還欠缺的第三個數,如果存在且滿足一定條件則將這個組合加入最終結果中。

大佬程式碼的優點:

將最終對三個元素的獲取的遍歷範圍限定在最小區間內,完全不需要遍歷所有元素,只需要遍歷0左右其中一個區間的元素即可,而且在得到第一個值要獲取第二個值的時候還對第二個值的區間進一步地收縮:poses[basej] < minp,以此達到最快的遍歷速度.

大佬程式碼的特點:

在程式碼階段就對要進行遍歷的數的區間進行限定而不是執行的時候根據條件判斷進行,大大減少了遍歷的次數

相關文章