update:2023-3-26
Melkman算法无需进行数据预处理,只需要给出点集合,算法会自动处理。
实现代码待修改,无需进行快排。


Melkman算法是1985年Avraham A. Melkman在其在线简单折线的凸包的在线构造中引入的。他提出了复杂度为O(n)的增量算法计算。简单之处是使用了双向表的方法来实现。

算法思想:

  1. 逐个比较所给点的y坐标值,选出y坐标最大的点,若两个点的y坐标值相同时,取x坐标最小的点,并将该点作为该点集的第一个元素
  2. 计算出其他所有点与y坐标最小点的斜率,并将点集按照斜率从小到大排序:
  3. 第一次首先找出第一个点的左右相邻的两个点,构成三角形,如果初始的点与该点相邻的两个点共线,则依次找下一个点;
  4. 整个查找按照逆时针方向,判断当前点是不是在凸包内部,则去掉上一个点(因为该点在凸包内),将当前点添加到凸包上的点集中;
  5. 直到查找到最后一个点结束,此时该点集的左右两端为同一点,即形成闭包。

注:首先将初始点(y坐标最小的点)放在所求点集的中间,分别向该点的左右两边添加元素。因为该双向表的两端的元素相同,所以满足上一点的两条边和当前点与上一点连成的直线均向左转则判定点在直线内。

C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#include<iostream>
#include<sstream>
#include<cstring>
#include<fstream>
using namespace std;
#define MaxSize 500

/**
* 建立点的坐标的结构体
*/
typedef struct
{
double x;
double y;
double angle;
}Point;

Point points[MaxSize];//用于存储原始数据
Point results[MaxSize];//用于存储结果
int PointsNum = 0, xRange = 0, yRange = 0;//分别为点的个数,x的范围,y的范围
int index[MaxSize];//数组索引,双向表




/**
* 读取文件的方法
*/
int readData(string fn)
{
//string s;
int flag = 0;//作为判断是否为第一次读取的标志
//static int x[MaxSize] = { 0 }, y[MaxSize] = { 0 };
int i = 0;//计数
char t = NULL;//存储非数字的符号
ifstream fin(fn, ios::in);
if (fin.fail())
{
cout << "无法打开该文件";
return 0;
}
while (!fin.eof())
{
if (flag == 0)
{
//在第一次读取时分别读取点数,x的最大值,y的最大值,按行读取
fin >> PointsNum >> t >> xRange >> t >> yRange;
flag = 1;
}

/**
* 分别读取点的坐标,t作为临时变量存储括号和逗号
*/
int a, b;
fin >> t >> a >> t >> b >> t;
points[i].x = a;
points[i].y = b;
i++;
}
return i - 1;

}

/**
* 信息写入txt文件的方法
*/
void wtriteData(string output)
{

}
/**
* 点集的元素位置交换
*
*/
void swap(int i, int j)
{
Point tempPoint;
tempPoint.x = points[j].x;
tempPoint.y = points[j].y;
tempPoint.angle = points[j].angle;

points[j].x = points[i].x;
points[j].y = points[i].y;
points[j].angle = points[i].angle;

points[i].x = tempPoint.x;
points[i].y = tempPoint.y;
points[i].angle = tempPoint.angle;
}

/**
* 移动起点
* 返回移动后的位置
*/
int loc(int top, int bot)
{
double x = points[top].angle;
int j, k;
j = top + 1;
k = bot;
while (true)
{
while (j < bot && points[j].angle < x)
j++;
while (k > top && points[k].angle > x)
k--;
if (j >= k)
break;
swap(j, k);
}
swap(top, k);
return k;
}

/**
* 快速排序
*/
void quickSort(int top, int bot)
{
int pos;
if (top < bot)
{
pos = loc(top, bot);
quickSort(top, pos - 1);
quickSort(pos + 1, bot);

}
}

/**
* 判断是否为左转
* 通过行列式D=(x1-x2)*(y3-y2)-(x3-x2)*(y1-y2)得到
* 当行列式值为正时,则反时针(左)转,当行列式值为负时,则顺时针(右)转。行列式值为0当且仅当三点共线。
*/
double isLeft(Point o, Point a, Point b)
{
double aoX = a.x - o.x;
double aoY = a.y - o.y;
double baX = b.x - a.x;
double baY = b.y - a.y;
return aoX * baY - aoY * baX;
}

void getResults(Point points[], int PointsNum)
{
int k = 0;
Point Ymin;
Ymin.x = 32767;
Ymin.y = 32767;
for (int i = 0; i < PointsNum; i++)
{
/**
* 找出y值最小的点
* 返回点的序号
*/
if (points[i].y < Ymin.y)
{
Ymin.x = points[i].x;
Ymin.y = points[i].y;
k = i;
}
/**
* 如果两个点的x坐标相等
* 则判断两个点的y坐标,谁的y坐标小
*/
if (points[i].y == Ymin.y)
{
if (points[i].x < Ymin.x)
{
Ymin.x = points[i].x;
Ymin.y = points[i].y;
k = i;
}
}
}
//将y坐标最小的元素与points[0]替换
swap(0, k);
//求其余顶点与y坐标最小的点间的直线与x轴的夹角
for (int i = 1; i < PointsNum; i++)
{
/**
* 求该直线的斜率
* 由于points[i].x - points[0].x可能小于0,
* 所以取绝对值
*/
double t = (points[i].y - points[0].y) / abs(points[i].x - points[0].x);
//通过反正切函数求得角度
points[i].angle = atan(t);
}
//根据角度进行快速排序
quickSort(1, PointsNum - 1);

int bot = PointsNum - 1;
int top = PointsNum;
index[top++] = 0;//index[PointsNum]=0;
index[top++] = 1;//index[PointsNum+1]=1;top=PointsNum+2;
int i;
for (i = 2; i < PointsNum; i++)
{
/**
*寻找第3个点
* 判断points[0],points[1],points[2]三点是否共线
*/
if (isLeft(points[index[top - 2]], points[index[top - 1]], points[i]) != 0)
{
break;
}
//如果共线就更换顶点
index[top - 1] = i;
}
index[bot--] = i;//index[PointsNum-1]=i;bot=PointsNum - 2;
index[top++] = i;//index[PointsNum+2]=i;top=PointsNum+3;

//第一次运行
int t;
if (isLeft(points[index[PointsNum]], points[index[PointsNum + 1]], points[index[PointsNum + 2]]) < 0)
{
//检测这三个点是否为逆时针方向,如果不是,则调换位置
t = index[PointsNum];
index[PointsNum] = index[PointsNum + 1];
index[PointsNum + 1] = t;
}

for (i++; i < PointsNum; i++)
{ //如果成立,则说明i在凸包内,跳过
//此时top=PointsNum+3,bot=PointsNum - 1
if (isLeft(points[index[top - 2]], points[index[top - 1]], points[i]) > 0 && isLeft(points[index[bot + 1]], points[index[bot + 2]], points[i]) > 0)
continue;
//非左转,退栈
while (isLeft(points[index[top - 2]], points[index[top - 1]], points[i]) <= 0)
top--;
index[top++] = i;

//非左转,退栈
while (isLeft(points[index[bot + 1]], points[index[bot + 2]], points[i]) <= 0)
bot++;
index[bot--] = i;
}
//在index数组里,bot+1到top-1内的就是凸包的点集
int index1 = 0;
for (i = bot + 1; i < top - 1; i++)
{
results[index1++] = points[index[i]];
cout << points[index[i]].x << " " << points[index[i]].y << endl;
}
}





int main()
{
int len = readData("./points5.txt");
getResults(points, PointsNum);
cout << "-------------------------------------------------" << endl;
for (int i = 0; i < PointsNum; i++)
cout << points[i].x << " " << points[i].y << endl;
return 0;
}

参考文献:

[1]凸包算法剖析[EB/OL]. /2021-01-29. https://cyw3.github.io/YalesonChan/2016/ConvexHull.html.

[2]陈道蓄. 平面上的凸包计算[J]. 中国信息技术教育, 2020(21): 25–29.