面试题 16.03. 交点
给定两条线段(表示为起点start = {X1, Y1}和终点end = {X2, Y2}),如果它们有交点,请计算其交点,没有交点则返回空值。
要求浮点型误差不超过10^-6。若有多个交点(线段重叠)则返回 X 值最小的点,X 坐标相同则返回 Y 值最小的点。
输入:
line1 = {0, 0}, {1, 0}
line2 = {1, 1}, {0, -1}
输出: {0.5, 0}
来源:https://leetcode-cn.com/problems/intersection-lcci/
题解:
- 排除界限不重合的情况
if (Math.max(a[0], b[0]) < Math.min(c[0], d[0]) ||
Math.max(c[0], d[0]) < Math.min(a[0], b[0]) ||
Math.max(a[1], b[1]) < Math.min(c[1], d[1]) ||
Math.max(c[1], d[1]) < Math.min(a[1], b[1])) {
return [];
}
- 根据叉乘的正负判断两个点是否在一条线段的两侧,原理见下图
/* 获取向量叉乘值 sx x sy */
function getCross(s, x, y) {
let x1, y1, x2, y2;
[x1, y1] = [x[0] - s[0], x[1] - s[1]];
[x2, y2] = [y[0] - s[0], y[1] - s[1]];
return x1 * y2 - x2 * y1;
}
let acb = getCross(a, c, b);//ac x ab
let adb = getCross(a, d, b);//ad x ab
let cad = getCross(c, a, d);//ca x cd
let cbd = getCross(c, b, d);//cb x cd
- 确保有交点后根据y=kx+b根据数学公式获取两条线段的k和b,提前排序是为了垂直时后面可以直接取到最小值
/* 统一端点顺序 */
if (a[0] > b[0] || a[0] == b[0] && a[1] > b[1]) [a, b] = [b, a];
if (c[0] > d[0] || c[0] == d[0] && c[1] > d[1]) [c, d] = [d, c];
/* k = (y2 - y1) / (x2 - x1) */
let k1 = (b[1] - a[1]) / (b[0] - a[0]);
let k2 = (d[1] - c[1]) / (d[0] - c[0]);
/* b = (x2 * y1 - x1 * y2) / (x2 - x1) */
let b1 = (b[0] * a[1] - a[0] * b[1]) / (b[0] - a[0]);
let b2 = (d[0] * c[1] - c[0] * d[1]) / (d[0] - c[0]);
- 对于共线情况,遍历4个点,先判断是否是公共点,再根据题目要求选x最小的或x相等y最小的
/* 判断斜率相同情况下x,y是否在两条线段上 */
function isInLine(x, y) {
for (let [s, e] of [[a, b], [c, d]]) {
/* 求边界值 */
let [maxx, maxy] = [Math.max(s[0], e[0]), Math.max(s[1], e[1])];
let [minx, miny] = [Math.min(s[0], e[0]), Math.min(s[1], e[1])];
if (x < minx || x > maxx || y < miny || y > maxy)
return false;
}
return true;
}
/* 共线 包括垂直的情况 */
if (k1 == k2) {
let x = null, y = null;
for (let [xx, yy] of [a, b, c, d]) {
if (isInLine(xx, yy)) {
if (x == null && y == null ||
xx < x || xx == x && yy < y) {
x = xx; y = yy;
}
}
}
return [x, y];
}
- 若只有一条线垂直,直接取垂直线的横坐标,计算y值,若没有垂直的线,带公式求x,y即可
else if (k1 === Infinity) {//ab垂直
return [a[0], k2 * a[0] + b2];
}
else if (k2 === Infinity) {//cd垂直
return [c[0], k1 * c[0] + b1];
}
else {//正常相交情况
let x = (b2 - b1) / (k1 - k2);
return [x, x * k1 + b1];
}
- 完整代码
var intersection = function (a, b, c, d) {
/* 排除区间不相交的情况 */
if (Math.max(a[0], b[0]) < Math.min(c[0], d[0]) ||
Math.max(c[0], d[0]) < Math.min(a[0], b[0]) ||
Math.max(a[1], b[1]) < Math.min(c[1], d[1]) ||
Math.max(c[1], d[1]) < Math.min(a[1], b[1])) {
return [];
}
/* 获取向量叉乘值 sx x sy */
function getCross(s, x, y) {
let x1, y1, x2, y2;
[x1, y1] = [x[0] - s[0], x[1] - s[1]];
[x2, y2] = [y[0] - s[0], y[1] - s[1]];
return x1 * y2 - x2 * y1;
}
let acb = getCross(a, c, b);//ac x ab
let adb = getCross(a, d, b);//ad x ab
let cad = getCross(c, a, d);//ca x cd
let cbd = getCross(c, b, d);//cb x cd
/*有交点 b,c在ab两侧 ab在cd两侧*/
if (acb * adb <= 0 && cad * cbd <= 0) {
/* 统一端点顺序 */
if (a[0] > b[0] || a[0] == b[0] && a[1] > b[1]) [a, b] = [b, a];
if (c[0] > d[0] || c[0] == d[0] && c[1] > d[1]) [c, d] = [d, c];
/* k = (y2 - y1) / (x2 - x1) */
let k1 = (b[1] - a[1]) / (b[0] - a[0]);
let k2 = (d[1] - c[1]) / (d[0] - c[0]);
/* b = (x2 * y1 - x1 * y2) / (x2 - x1) */
let b1 = (b[0] * a[1] - a[0] * b[1]) / (b[0] - a[0]);
let b2 = (d[0] * c[1] - c[0] * d[1]) / (d[0] - c[0]);
/* 判断斜率相同情况下x,y是否在两条线段上 */
function isInLine(x, y) {
for (let [s, e] of [[a, b], [c, d]]) {
/* 求边界值 */
let [maxx, maxy] = [Math.max(s[0], e[0]), Math.max(s[1], e[1])];
let [minx, miny] = [Math.min(s[0], e[0]), Math.min(s[1], e[1])];
if (x < minx || x > maxx || y < miny || y > maxy)
return false;
}
return true;
}
/* 共线 包括垂直的情况 */
if (k1 == k2) {
let x = null, y = null;
for (let [xx, yy] of [a, b, c, d]) {
if (isInLine(xx, yy)) {
if (x == null && y == null ||
xx < x || xx == x && yy < y) {
x = xx; y = yy;
}
}
}
return [x, y];
}
else if (k1 === Infinity) {//ab垂直
return [a[0], k2 * a[0] + b2];
}
else if (k2 === Infinity) {//cd垂直
return [c[0], k1 * c[0] + b1];
}
else {//正常相交情况
let x = (b2 - b1) / (k1 - k2);
return [x, x * k1 + b1];
}
}
/* 没有交点 */
return [];
};