我的C实践(3):用宏和位运算来实现整数集合

    整数集合用一个位向量来表示。这里是无符号整数的集合,它适合存放范围在0~N-1之内的小整数,N是位向量的位数(这里为32位的unsigned int)。每个整数用集合中的一个位来表示,第i位为1(i=0~N-1),当且仅当整数i属于集合。
    1、头文件set.h:整数集合的接口。

/* 
 * set.h:整数集合,是一个位向量。适合存放范围在0~N-1之内的小整数,N是unsigned int的
 * 位数(32位)。每个整数用集合中的一个位来表示。第i位为1(i=0~N-1),当且仅当i属于集合。
 */
#ifndef UINT_SET_H
#define UINT_SET_H
#include <limits.h>   /* 用到CHAR_BIT */

/* 类型SET用来代表集合 */
typedef unsigned int SET;

/* SET_BITS: 每个集合的最大位数 */
#define SET_BITS  (sizeof(SET)*CHAR_BIT)

/* check(i): 判断i是否能够成为集合的一个元素 */
#define check(i)  (((unsigned) (i)) < SET_BITS)

/* emptyset: 返回无任何元素的空集 */
#define emptyset  ((SET) 0)
/* singleset(i): 返回只含有一个整数i的集合 */
#define singleset(i)  (((SET) 1) << (i))

/* add(s,i): 向集合set中添加一个整数i,将第i位置为1即可 */
#define add(set,i)  ((set) | singleset (i))

/* intersect: 返回两个集合的交集 */
#define intersect(set1,set2)  ((set1) & (set2))

/* union: 返回两个集合的并集 */
#define union(set1,set2)  ((set1) | (set2))

/* setdiff: 返回两个集合的对称差(只属于其中一个集合,而不属于另一个集合的元素) */
#define setdiff(set1,set2)   ((set1) ^ (set2))

/* element: 判断i是否在集合set中 */
#define element(i,set)  (singleset((i)) & (set))

/* forallelements: 遍历集合s中的每个元素,以便执行一个语句。例如,为了打印
    集合S中的每个整数,可以这样写:
   int j;
   forallelements(j, s)
      printf("%d ", j);
*/ 
#define forallelements(j,s) /
	for ((j)=0; (j)<SET_BITS; ++(j)) if (element((j),(s)))

/* first_set_of_n_elements(n): 生成集合{0,1,...,n-1},
    这利用了无符号整数的减法性质 */
#define first_set_of_n_elements(n)	(SET)((1<<(n))-1) 

/* next_set_of_n_elements(s): 对集合s,产生一个大小相等的新集合,它与x必定不相同。
   如果你从first_set_of_n_elements(k)的结果{0,1,...,k-1}开始,在前面的结果上不断地
   应用next_set_of_n_elements,直到产生的集合包含了整数m。这样你就得到{0,1,...,m-1}
   的所有大小为k的子集,其个数为“从m件东西中选出k件东西的所有可能的组合数目”,它应该等于
   m!/(k!(m-k)!) */
extern SET next_set_of_n_elements(SET x); 

/* printset(s): 打印集合x的所有元素,输出格式为{1, 2, 3, 4} */
extern void printset(SET z);

/* cardinality(s): 返回集合x的基数(即元素个数) */
extern int cardinality(SET x);

/* print_k_of_n(k,n): 打印集合{0,1,...,n-1}的所有大小为k的子集,每一行会打印尽可能
    多的子集。同时也会打印这些子集的总个数,它应该等于n!/(k!(n-k)!) */
extern void print_k_of_n(int k, int n);
#endif

    解释:
    (1)singleset(i):由于要返回只含有一个整数i的集合,因此要将位向量的第i位置1,其余位均为0,只要将1右移i位即可。
    (2)first_set_of_n_elements(n):要生成集合{0,1,...,n-1},就要将第0,1,...,n-1位置1,其余位置0。可将1右移n位,这样第n位为1,位向量的值为2**(n-1),这里**为幂。减1后就变成2**(n-1)-1,其第0,1,...,n-1位为1,其余位为0。
    2、函数定义文件set.c:整数集合的各个函数实现。

/* 
 * set.c:整数集合的各个函数实现 
 */
#include <stdio.h>
#include "set.h"

/* 返回集合x的基数(即元素个数) */
int cardinality(SET x){
	int count = 0;
	while (x != emptyset) {
		/* -x会对x取反并加1,x & -x获得x的最右边的位1(其余位变成0),
		   x^(x&-x)会将x最右边的位1置成0,因此就去掉了集合中的最小元素 */
		x ^= (x & -x);
		++count;
	}
	return count;
}

/* 对集合x,产生一个大小相等的新集合,它与x必定不相同 */
SET next_set_of_n_elements(SET x){
/* 这里的代码利用了无符号运算的一些技巧,例子如下:
      如果 x              == 001011001111000, 则
     smallest           == 000000000001000
     ripple             == 001011010000000
     new_smallest       == 000000010000000
     ones               == 000000000000111
     the returned value == 001011010000111 
    基本思想是找到x中最右边的连续位1组,把这个组最左边的位1向左滑动一位,
    把其余的所有1都滑到最右端 */
	SET smallest, ripple, new_smallest, ones;	
	if(x == emptyset) 
		return x;
	
	smallest     = (x & -x); /* 得到只含x最右边的位1的数(其余位为0) */
	ripple       = x + smallest; /* 会把x最右边的连续位1组清0,并且组的左边一位置1,这样组的
	                                        最左边位1就向左滑了一位,这个位成为ripple最右边的位1 */
	new_smallest = (ripple & -ripple); /* 得到只含ripple最右边的位1的数 */	
	/* 将new_smallest的这个位1滑到连续位1组的左边,然后将其后面的所有位都置1(其余位变成0) */
	ones         = ((new_smallest / smallest) >> 1) - 1;
	return (ripple | ones); /* 得到最后结果并返回 */
} 

/* 打印集合z的所有元素,输出格式为{1, 2, 3, 4} */
void printset(SET z){
	int first = 1; /* 表示集合打印的开始 */
	int e;
	forallelements(e, z) {
		if(first) 
			printf("{");
		else
			printf(", ");
		printf("%d", e);
		first = 0;
	}
	if (first) printf("{"); /* 上面没有执行,说明集合中无元素,是空集 */
	printf("}");            /* 打印集合的闭括号 */
}

#define LINE_WIDTH 54   /* 输出行的行宽 */
/* 打印集合{0,1,...,n-1}的所有大小为k的子集 */
void print_k_of_n(int k, int n){
	int count = 0;
	/* 集合打印时占的宽度,为k*4+3或k*3+3 */
	int printed_set_width = k * ((n > 10) ? 4 : 3) + 3;
	/* 每行打印的集合个数 */
	int sets_per_line = LINE_WIDTH / printed_set_width;
	
	SET z=first_set_of_n_elements(k); /* 生成集合{0,1,...,k-1} */
	
	/* 生成集合{0,1,...,n-1},然后打印其所有大小为k的子集 */
	printf("/nAll the size-%d subsets of ", k);
	printset (first_set_of_n_elements(n)); 
	printf(":/n");
	
	do{	 /* 打印{0,1,...,n-1}的所有大小为k子集 */
		printset(z); 
		if((++count) % sets_per_line) 
			printf (" ");
		else 
			printf("/n");  /* 一行已满时,换行 */
		z = next_set_of_n_elements(z); /* 产生一个大小为k的新子集 */
	}while((z != emptyset) && !element(n, z)); /* 一旦产生的集合为空集或包含了元素n,
	                                                          说明不再是子集,结束循环 */
	
	if ((count) % sets_per_line) printf ("/n");
	printf("The total number of such subsets is %d./n",
		count);
}

    解释:

    (1)cardinality(SET x)用于返回集合中元素的个数,采用的方法每去掉一个最小元素,就让计数器增1。这里-x会对x取反并加1,x & -x获得x的最右边的位1(其余位变成0),x^(x & -x)会将x最右边的位1置成0,其余位不变,因此就去掉了集合中的最小元素。
    (2)printset(SET z)用于打印集合中所有元素。它用forallelements(e, z)来遍历集合的每个元素,然后打印它。
    (3)print_k_of_n(k,n)用于打印集合{0,1,...,n-1}的所有大小为k的子集。它先设置打印参数,然后用first_set_of_n_elements(k)生成初始的大小为k的子集z={0,1,...,k-1}。打印各个子集时,从初始的子集z开始,不断地应用next_set_of_n_elements(z)生成大小为k的新子集,并用printset打印它。一旦产生的集合为空集或包含了元素n,显然这时就不再是{0,1,...,n-1}的子集了,结束循环。
    (4)关键看next_set_of_n_elements(SET x)的实现。它必须保证生成大小为k但与z不相同的子集,同时要保证当生成空集或生成包含了元素n的集合时,{0,1,...,n-1}的所有的大小为k的子集(个数为n!/(k!(n-k)!)都已经产生。采用的算法在注释中已经描述得比较清楚了。看一个例子,对于{0,1,2,3,4}的大小为3的所有子集,从初始子集{0,1,2}开始,由于算法只对最小的连续相邻的数(构成一个位1组)进行移动,因此它能遍历到所有大小为3的子集。对组中最右边的2向左移一位,组中其余的位1移到最右端,得{0,1,3},不停地应用该算法,得{0,2,3},{1,2,3},{0,1,4},....,{2,3,4},再移动就得{0,1,5},生成含有元素5的集合,不再是子集了,因此循环就结束。
    3、测试文件testset.c:对整数集合的测试。

/* 
 * testset.c 对整数集合的测试
 */
#include "set.h"
int main(void){
	print_k_of_n(0, 4); 
	print_k_of_n(1, 4);
	print_k_of_n(2, 4); /* 打印集合{0,1,2,3}的所有大小为2的子集 */
	print_k_of_n(3, 4);
	print_k_of_n(4, 4);
	print_k_of_n(3, 5);
	print_k_of_n(3, 6);
	return 0;
}

转载于:https://my.oschina.net/abcijkxyz/blog/723205

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值