0.写在前面
夏树大一上学期为时一周的C语言课设就在昨天周五结束了。其中印象比较深刻的就是课设中有一题运用Kmeans算法对点集实现分组的题目啦(顺便自己试着摸了下GDI的一些函数),然后今天抽出些时间来整理一下,先上最后实现的结果:


1.问题描述
随机生成一组位于二维坐标系的点集(15<=点集元素个数N<=65),这些点不重合,每个点的位置由x,y值决定,x,y为整数且0<x<80,0<y<40。现在想知道这些点按距离远近该分成几个组合适,已知分组值K备选范围从1-10,输入K值,请你按以下方法画出分组结果:
- (1) 从N个数据点中随机挑选K个不同点作为K个组的初始中心起点(组中心);
- (2) 计算所有点到K个组中心的距离(欧式距离),并把它归到距离最近的组(如果有多个组距离一样,随便选一个归入);
- (3) 更新K个组的中心值(求出该组所有点的x平均值和y平均值,作为新的组中心);
- (4) 重复(2)~(3)步直至新的组中心和原来的组中心差值小于指定阈值(人为设置),或超出最大重复次数(人为设置),算法结束;
- (5) 在控制台画出K组分组结果,每组用不同数字表示,使用0,1,2,3….9分别表示10个组。
2.解决思路
2.1.随机生成不重复的二维坐标点集
首先根据题目的意思,生成15-65个点组成二维坐标系的点集(这里有自定义了一个查询重复点坐标的函数int isRandCoordRepeat(int x,int y,COORD coord[],int n);):
/*初始化随机坐标数组*/
/*返回生成的坐标数量*/
int initRandCoord(COORD coord[], int minN, int maxN)
{
int x,y;
int cnt=0,i,n;
srand((unsigned)time(0));
n = rand()%(maxN+1)+minN;
srand((unsigned)time(0));
for(i=0;i<n;i++)
{
x = rand()%80+1;
y = rand()%40+1;
while(isRandCoordRepeat(x,y,coord,cnt))
{
x = rand()%80+1;
y = rand()%40+1;
}
coord[i].X = x;
coord[i].Y = y;
cnt++;
if(cnt>=maxN)
{
break;
}
}
return cnt;
}
2.2初始化k个中心点
这边定义了一个结构体CENTER用来存储中心点变量的一些结构类型, 但是存储点集分组情况的变量g_group和存储点集坐标的变量g_coord是分开的(做到后面有点点懒,就没有再新定义结构体了,不然有点上头)
struct CENTER{
int X;
int Y;
int tryCnt; //之后用来存储微调中心点的重复次数
int flag; //单个点能否继续微调的标志
int color; //GDI绘制该分组情况的颜色
}
由用户输入k值,从刚刚生成的点集中挑选k个作为最初的中心点
/*初始化中心点*/
void initCenterDot(COORD coord[], int cnt, struct CENTER center[], int k)
{
int i,rndNum,x,y;
COORD centerTmp[10];
srand((unsigned)time(0));
/*无重复初始化中心点*/
for(i=0;i<cnt;i++)
{
rndNum = rand()%cnt;
x = coord[rndNum].X;
y = coord[rndNum].Y;
while(isRandCoordRepeat(x,y,centerTmp,i+1))
{
rndNum = rand()%cnt;
x = coord[rndNum].X;
y = coord[rndNum].Y;
}
centerTmp[i].X = x;
centerTmp[i].Y = y;
if(i>k)
{
break;
}
}
/*将中心点坐标赋值到全局变量*/
for(i=0;i<k;i++)
{
g_center[i].X = centerTmp[i].X;
g_center[i].Y = centerTmp[i].Y;
g_center[i].tryCnt = 1;
g_center[i].flag = 1;
g_center[i].color = 0;
packedDrawDot(centerTmp[i].X,centerTmp[i].Y,RGB(255,0,0),g_hdc);
}
}
2.3.对点集进行分组
下面的代码中,夏树封装了一些使用GDI划线、画点以及输出文字的函数(方便自己用吧。不过如果是作业的VC下也有EasyX封装的绘图可以用,感兴趣的话可以自己了解一下?)
HDC packedGetDC(); void packedDrawDot(int x,int y,int color,HDC hdc); void packedDrawLine(int xS,int yS, int xE, int yE, int color,HDC hdc); void packedPrintText(int x,int y,char str[],int len,int color,HDC hdc);
然后计算所有点到每个中心点的距离,将点归到距离最近的中心点的分组中(因为课设题目中最多只分成10组, 下面输出组数的时候偷懒了一下)。
/*分组中心点(第n次分组)*/
void groupCenterDot(COORD coord[], int cnt, struct CENTER center[], int k)
{
char cnum[5]={0};
int i,j,minI,len;
double distance,minDistance;
/*设置分组颜色*/
srand((unsigned)time(0));
for(i=0;i<k;i++)
{
if(!center[i].color){
center[i].color = RGB(rand()%255+1,rand()%255+1,rand()%255+1);
}
}
/*计算到中心点最短距离并分组*/
for(i=0;i<cnt;i++)
{
minDistance=-1;
minI=-1;
for(j=0;j<k;j++)
{
distance = (int)sqrt((coord[i].X-center[j].X)*(coord[i].X-center[j].X)+(coord[i].Y-center[j].Y)*(coord[i].Y-center[j].Y));
if(minDistance<0)
{
minDistance = distance;
minI = j;
continue;
}
if(distance<minDistance)
{
minDistance = distance;
minI = j;
}
}
g_group[i] = minI;
}
/*绘制所有点编号*/
for(i=0;i<cnt;i++)
{
j=g_group[i]+1;
len=0;
if(j<10)
{
cnum[0]=j+'0';
len=1;
}
else if(j==10)
{
cnum[0]='1';
cnum[1]='0';
len=2;
}
packedDrawLine(coord[i].X,coord[i].Y,center[g_group[i]].X,center[g_group[i]].Y,center[g_group[i]].color,g_hdc);
packedPrintText(coord[i].X,coord[i].Y,cnum,len,RGB(255,255,255),g_hdc);
}
}
2.4.微调分组中心点
计算各个分组中所有点x的平均值和y的平均值,以这个平均值作为新的中心点,在微调中心点后重复2.3的分组操作。
那么,怎么判断是否应该结束分组了呢?
夏树这边对单个点的结束分组做了两种判断: (1)当计算出的x的平均值与上次中心点的x相差在1以内(y同理) (2)否则, 单个中心点的分组次数+1, 当分组次数大于等于设置的MAXTRY(1000次)时结束该点的分组
当所有点的分组都结束时,结束分组,并返回2.3绘制分组结果
/*更新中心点*/
void trimCenterDot(COORD coord[], int cnt, struct CENTER center[], int k)
{
int i,j,groupNum,tag=0;
double avgX,avgY;
for(i=0;i<k;i++)
{
if(!center[i].flag) continue;
avgX = 0;
avgY = 0;
groupNum=0;
for(j=0;j<cnt;j++)
{
if(g_group[j]!=i) continue;
avgX+=coord[j].X;
avgY+=coord[j].Y;
groupNum++;
}
avgX/=groupNum;
avgY/=groupNum;
//误差及微调阈值
if((int)avgX-center[i].X<=DIFF && (int)avgY-center[i].Y<=DIFF)
{
center[i].flag = 0;
continue;
}
else
{
if(center[i].tryCnt>=MAXTRY)
{
center[i].flag = 0;
continue;
}
center[i].X = (int)avgX;
center[i].Y = (int)avgY;
center[i].tryCnt++;
}
}
//遍历所有中心点是否可以微调
for(i=0;i<k;i++)
{
if(center[i].flag)
{
tag=1;
return;
}
}
if(!tag)
{
g_tag = 0;
}
}
3.一些参考
4.结束语
大概思路就是这些啦, 虽然代码还是有点乱, 没有做到太好的规范, 夏树之后也会继续努力der
本文最终实现效果源代码 Github: https://github.com/natukicc/kmeans-best-grouping
(完)
?才发现题目给的编号是0-9呀,哎,假装没看见吧(就放着不太想改的样子啦)。