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呀,哎,假装没看见吧(就放着不太想改的样子啦)。