Jarvis 算法背后的想法非常简单。首先必须要从凸包上的某一点开始,比如从给定点集中最左边的点开始,例如最左的一点 A1 。然后选择 A2 点使得所有点都在向量 A1A2 的左方或者右方,我们每次选择左方,需要比较所有点以 A1 为原点的极坐标角度。然后以 A2 为原点,重复这个步骤,依次找到 A3,A4,…,Ak。 给定原点 p,如何找到点 q,满足其余的点 r 均在向量 pq 的左边,我们使用「向量叉积」来进行判别。我们可以知道两个向量 pq , qr 的叉积大于 0 时,则两个向量之间的夹角小于 180 ,两个向量之间构成的旋转方向为逆时针,此时可以知道 r 一定在 pq 的左边;叉积等于 0 时,则表示两个向量之间平行,p,q,r 在同一条直线上;叉积小于 0 时,则表示两个向量之间的夹角大于 180 ,两个向量之间构成的旋转方向为顺时针,此时可以知道 r 一定在 pq 的右边。为了找到点 q,我们使用函数 cross() ,这个函数有 3 个参数,分别是当前凸包上的点 p,下一个会加到凸包里的点 q,其他点空间内的任何一个点 r,通过计算向量 pq , qr 的叉积来判断旋转方向,如果剩余所有的点 r 均满足在向量 pq 的左边,则此时我们将 q 加入凸包中。下图说明了这样的关系,点 r 在向量 pq 的左边。
从上图中,我们可以观察到点 p,q 和 r 形成的向量相应地都是逆时针方向,向量 pq 和 qr 旋转方向为逆时针,函数 cross(p,q,r) 返回值大于 0。
我们遍历所有点 r,找到对于点 p 来说逆时针方向最靠外的点 q,把它加入凸包。如果存在 2 个点相对点 p 在同一条线上,我们应当将 q 和 p 同一线段上的边界点都考虑进来,此时需要进行标记,防止重复添加。
class Solution {
public int[][] outerTrees(int[][] trees) {
int n = trees.length;
if (n < 4) {
return trees;
}
int leftMost = 0;
for (int i = 0; i < n; i++) {
if (trees[i][0] < trees[leftMost][0]) {
leftMost = i;
}
}
List<int[]> res = new ArrayList<int[]>();
boolean[] visit = new boolean[n];
int p = leftMost;
do {
int q = (p + 1) % n;
for (int r = 0; r < n; r++) {
/* 如果 r 在 pq 的右侧,则 q = r */
if (cross(trees[p], trees[q], trees[r]) < 0) {
q = r;
}
}
/* 是否存在点 i, 使得 p 、q 、i 在同一条直线上 */
for (int i = 0; i < n; i++) {
if (visit[i] || i == p || i == q) {
continue;
}
if (cross(trees[p], trees[q], trees[i]) == 0) {
res.add(trees[i]);
visit[i] = true;
}
}
if (!visit[q]) {
res.add(trees[q]);
visit[q] = true;
}
p = q;
} while (p != leftMost);
return res.toArray(new int[][]{});
}
public int cross(int[] p, int[] q, int[] r) {
return (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]);
}
}
复杂度分析
时间复杂度:O(n^2),其中 n 为数组的长度。每次判定一个点 p,同时需要遍历数组所有点,一共最多需要取出 n 个点,因此时间复杂度为 O(n^2)。
空间复杂度:O(n)。需要对每个点进行标记,需要的空间复杂度为 O(n)。