1. 问题分析与解题思路
今天我们来解决LeetCode第973题"K Closest Points to Origin"。这道题要求我们从一个二维平面上的点集中找出距离原点(0,0)最近的k个点。乍一看似乎很简单,但其中包含了不少值得深入探讨的算法细节和优化技巧。
1.1 问题重述
给定一个二维点集points,其中每个点表示为[x_i, y_i],以及一个整数k。我们需要返回距离原点最近的k个点。距离的计算采用欧几里得距离公式:
distance = √(x² + y²)
但题目特别说明,答案可以以任意顺序返回,且保证唯一(除了顺序不同外)。
1.2 关键观察点
在开始编码前,有几个重要的观察点:
-
平方根计算的优化:由于我们只需要比较距离的相对大小,而不需要实际的精确距离值,因此可以省略平方根计算,直接比较x²+y²的值。这能显著提升性能,因为平方根计算相对耗时。
-
排序与部分排序的选择:题目要求的是前k个最近的点,并不需要完整的排序。这意味着我们可以考虑使用部分排序算法,如快速选择(Quickselect),它的平均时间复杂度是O(n),比完整排序的O(nlogn)更优。
-
内存管理:C语言中需要特别注意内存分配和释放,特别是题目要求返回的数组和列大小数组都需要动态分配。
2. 解决方案设计与实现
2.1 基础解法:完整排序
我们先来看一个直观的解法——完整排序所有点,然后取前k个。
2.1.1 数据结构设计
我们需要一个辅助结构来存储点的索引和距离信息:
c复制typedef struct {
int idx; // 点在原数组中的索引
int dist; // 到原点的平方距离
} PointInfo;
2.1.2 比较函数实现
为了使用qsort进行排序,我们需要实现比较函数:
c复制static int cmpPointInfo(const void *a, const void *b) {
const PointInfo *pa = (const PointInfo *)a;
const PointInfo *pb = (const PointInfo *)b;
if (pa->dist < pb->dist) return -1;
if (pa->dist > pb->dist) return 1;
return 0;
}
2.1.3 主函数实现
c复制int** kClosest(int** points, int pointsSize, int* pointsColSize,
int k, int* returnSize, int** returnColumnSizes) {
// 步骤1:构建辅助数组,存储索引和平方距离
PointInfo *infos = (PointInfo *)malloc(pointsSize * sizeof(PointInfo));
for (int i = 0; i < pointsSize; ++i) {
long long x = points[i][0];
long long y = points[i][1];
long long d = x * x + y * y;
infos[i].idx = i;
infos[i].dist = (int)d; // 安全:最大值是2*10^8,在int范围内
}
// 步骤2:按距离排序
qsort(infos, pointsSize, sizeof(PointInfo), cmpPointInfo);
// 步骤3:准备返回结构
*returnSize = k;
int **res = (int **)malloc(k * sizeof(int *));
int *colSizes = (int *)malloc(k * sizeof(int));
for (int i = 0; i < k; ++i) {
colSizes[i] = 2; // 每个点有2个坐标
res[i] = (int *)malloc(2 * sizeof(int));
int idx = infos[i].idx;
res[i][0] = points[idx][0];
res[i][1] = points[idx][1];
}
*returnColumnSizes = colSizes;
free(infos);
return res;
}
注意:这里使用了long long类型来计算平方距离,是为了防止整数溢出。虽然题目给定的坐标范围是-10^4到10^4,最大平方距离是2*10^8,在int范围内,但良好的习惯是预防潜在的溢出问题。
2.2 优化解法:快速选择
完整排序的时间复杂度是O(nlogn),而实际上我们只需要前k个最小元素,可以使用快速选择算法将时间复杂度降低到平均O(n)。
2.2.1 快速选择算法原理
快速选择是快速排序的变种,它只对包含目标元素的子数组进行递归排序,而不是全部。基本步骤:
- 选择一个pivot元素
- 将数组分为小于pivot和大于pivot的两部分
- 根据pivot的位置决定继续处理哪一部分
2.2.2 实现代码
c复制// 分区函数,用于快速选择
int partition(PointInfo* arr, int left, int right) {
PointInfo pivot = arr[right];
int i = left;
for (int j = left; j < right; j++) {
if (arr[j].dist < pivot.dist) {
PointInfo temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
}
}
PointInfo temp = arr[i];
arr[i] = arr[right];
arr[right] = temp;
return i;
}
// 快速选择实现
void quickSelect(PointInfo* arr, int left, int right, int k) {
if (left >= right) return;
int pivotIndex = partition(arr, left, right);
if (k == pivotIndex) {
return;
} else if (k < pivotIndex) {
quickSelect(arr, left, pivotIndex - 1, k);
} else {
quickSelect(arr, pivotIndex + 1, right, k);
}
}
// 修改后的kClosest函数
int** kClosest(int** points, int pointsSize, int* pointsColSize,
int k, int* returnSize, int** returnColumnSizes) {
PointInfo *infos = (PointInfo *)malloc(pointsSize * sizeof(PointInfo));
for (int i = 0; i < pointsSize; ++i) {
long long x = points[i][0];
long long y = points[i][1];
long long d = x * x + y * y;
infos[i].idx = i;
infos[i].dist = (int)d;
}
// 使用快速选择而不是完整排序
quickSelect(infos, 0, pointsSize - 1, k - 1);
*returnSize = k;
int **res = (int **)malloc(k * sizeof(int *));
int *colSizes = (int *)malloc(k * sizeof(int));
for (int i = 0; i < k; ++i) {
colSizes[i] = 2;
res[i] = (int *)malloc(2 * sizeof(int));
int idx = infos[i].idx;
res[i][0] = points[idx][0];
res[i][1] = points[idx][1];
}
*returnColumnSizes = colSizes;
free(infos);
return res;
}
3. 复杂度分析与比较
3.1 时间复杂度
-
完整排序方法:
- 计算平方距离:O(n)
- 排序:O(nlogn)
- 构建结果:O(k)
- 总计:O(nlogn)
-
快速选择方法:
- 计算平方距离:O(n)
- 快速选择:平均O(n),最坏O(n²)
- 构建结果:O(k)
- 总计:平均O(n),最坏O(n²)
3.2 空间复杂度
两种方法都需要:
- O(n)空间存储辅助数组
- O(k)空间存储结果
- 总计:O(n)
3.3 实际性能考虑
虽然快速选择有更好的平均时间复杂度,但在实际应用中:
- 对于小规模数据,完整排序可能更快,因为qsort实现通常高度优化
- 快速选择的最坏情况O(n²)虽然罕见,但在某些场景下可能成为问题
- 如果k接近n,完整排序可能更优
4. 边界条件与测试案例
4.1 重要边界条件
- k等于1或等于pointsSize
- 点在坐标轴上(一个坐标为0)
- 多个点有相同的距离
- 大输入测试(pointsSize接近10^4)
4.2 测试案例设计
c复制void testKClosest() {
// 测试案例1:示例1
int points1[2][2] = {{1,3}, {-2,2}};
int* p1[2] = {points1[0], points1[1]};
int colSizes1[2] = {2,2};
int returnSize1;
int* returnColSizes1;
int** res1 = kClosest(p1, 2, colSizes1, 1, &returnSize1, &returnColSizes1);
printf("Test 1: %d %d\n", res1[0][0], res1[0][1]); // 应输出-2 2
// 测试案例2:示例2
int points2[3][2] = {{3,3}, {5,-1}, {-2,4}};
int* p2[3] = {points2[0], points2[1], points2[2]};
int colSizes2[3] = {2,2,2};
int returnSize2;
int* returnColSizes2;
int** res2 = kClosest(p2, 3, colSizes2, 2, &returnSize2, &returnColSizes2);
printf("Test 2: [%d,%d] [%d,%d]\n", res2[0][0], res2[0][1], res2[1][0], res2[1][1]);
// 测试案例3:k等于数组长度
int points3[4][2] = {{1,1}, {2,2}, {3,3}, {4,4}};
int* p3[4] = {points3[0], points3[1], points3[2], points3[3]};
int colSizes3[4] = {2,2,2,2};
int returnSize3;
int* returnColSizes3;
int** res3 = kClosest(p3, 4, colSizes3, 4, &returnSize3, &returnColSizes3);
printf("Test 3: All points returned\n");
// 记得释放分配的内存
// ... 省略释放代码 ...
}
5. 内存管理与优化技巧
5.1 内存泄漏预防
在C语言中实现这类问题时,特别需要注意内存管理:
- 分配与释放对称:每个malloc都应该有对应的free
- 多层结构释放:对于二维数组,需要先释放内部数组,再释放外部数组
- 错误处理:在实际应用中应该检查malloc返回值是否为NULL
5.2 优化建议
- 避免不必要的数据复制:可以尝试在原数组上操作,而不是创建辅助数组
- 使用堆数据结构:维护一个大小为k的最大堆,可以在O(nlogk)时间内解决问题
- 内联函数:对于性能关键的部分,考虑使用内联函数减少函数调用开销
6. 扩展思考与实际应用
6.1 问题变种
- 找到距离任意给定点最近的k个点:只需调整距离计算方式
- 在动态点集中维护最近的k个点:可能需要更高级的数据结构如kd-tree
- 在高维空间中寻找最近邻:维度灾难会使问题更具挑战性
6.2 实际应用场景
- 地理位置服务:找到离用户最近的k个兴趣点
- 推荐系统:找到与目标用户最相似的k个用户
- 计算机图形学:点云数据处理中的邻域查询
在实际编码面试中,这类问题不仅考察算法能力,也考察对语言特性的掌握(如C中的内存管理)、边界条件处理以及沟通能力。建议在解决问题时,先明确思路,讨论时间和空间复杂度,再着手实现,最后讨论优化方向。