集合与字典(上)----集合

集合

集合是现代数学中一个非常重要的概念,在某些情况下又被称为类、族或者搜索。

但实际上,数学上的集合与计算机当中所说的集合有很大的区别。首先数学中的集合可以是无限集,即集合中的元素可以由无穷多个,但计算机当中是一定要保证集合为有限集的。其次,根据定义来说,集合中的元素是没有排列顺序的,但为了方便运算,我们要保证集合中的元素是有序排放的,并且集合中必须存放相同数据类型的数据元素,这样才能通过关系运算符><来保证元素的顺序。
除了基本的概念之外,我们还要知道集合的三种基本运算,即并运算,交运算,差运算。具体的定义就不在这里赘述了。


集合的实现

先来讲一下位向量集合,其实位向量是一个数组,数组中每一个元素都是一个二进制位(0或者1)。位向量中并不存储实际的数据而只存储数据的状态,而且这个状态必须具有二值性

计算机中每种数据都有一定的类型,每种类型的数据都只有有限个。这就从理论上确保了使用位向量表示数据状态的可行性。

我们来举一个非常简单的例子:


位向量表示法

A与B并运算其位向量

A与B交运算其位向量

A与B差运算其位向量

大致的意思明白了,那么该如何实现呢?

class VecSet{
    int* contain;
    int size;
public:
    VecSet(int MaxSize = DefaultSize);
    ~VecSet();
    
    void Add(int add);
    void Del(int del);
    void MakeEmpty();
    int GetSize() {return size;}
    bool IsMember(const int x);
    VecSet& operator+(VecSet& another);
    VecSet& operator-(VecSet& another);
    VecSet& operator*(VecSet& another);
    VecSet& operator=(VecSet& another);
    bool operator==(VecSet& another);
    friend ostream& operator<<(ostream& stream, VecSet& set);
};

类中的成员函数的实现:

/** VecSet构造函数 */
VecSet::VecSet(int MaxSize):size(MaxSize) {
    assert(MaxSize > 0);
    contain = new int[size];
    assert(contain != NULL);
    MakeEmpty();
}
/** VecSet析构函数 */
VecSet::~VecSet() {
    if(contain != NULL)
        delete [] contain;
}
/** VecSet主要成员函数 */
void VecSet::Add(int add) {
    assert(add >= 0 && add < size);
    contain[add] = 1;
}
void VecSet::Del(int del) {
    assert(del >= 0 && del < size);
    contain[del] = 0;
}
void VecSet::MakeEmpty() {
    for(int i=0; i < size; i++)
        contain[i] = 0;
}
bool VecSet::IsMember(const int x) {
    assert(x >= 0 && x < size);
    return contain[x] != 0;
}

其中,函数Add(int add)用于向位向量中添加数add,即把代表add的二进制位置为1;
函数Del(int del)用于从位向量中删除数del,即把代表del的二进制位置为0;
除了一些基本的函数实现之外,还有一些运算符的重载,是用于实现我们前面所提到的交运算,并运算与差运算的。

/** 运算符的重载方法 */
VecSet& VecSet::operator+(VecSet &another) {
    assert(this -> GetSize() == another.GetSize());
    VecSet* temp = new VecSet(this -> GetSize());
    for(int i=0; i < size; i++) {
        (*temp).contain[i] = contain[i]||another.contain[i];
    }
    return *temp;
}
VecSet& VecSet::operator-(VecSet &another) {
    assert(this -> GetSize() == another.GetSize());
    VecSet* temp = new VecSet(this -> GetSize());
    for(int i=0; i<size; i++) {
        // 逻辑运算符&&的运算规则是当&&左边的条件为假时,运算结果总为假,当且仅当运算符&&左边的二进制为1,且右边的
        // 二进制值为0时运算结果为1.所以在这里使用了一个!来辅助实现这种运算规则
        (*temp).contain[i] = contain[i] && (!another.contain[i]); 
    }
    return *temp;
}
VecSet& VecSet::operator*(VecSet &another) {
    assert(this -> GetSize() == another.GetSize());
    VecSet* temp = new VecSet(this -> GetSize());
    for(int i=0; i<size; i++) {
        (*temp).contain[i] = contain[i] && another.contain[i];
    }
    return *temp;
}
VecSet& VecSet::operator=(VecSet &another) {
    assert(this -> GetSize() == another.GetSize());
    VecSet* temp = new VecSet(this -> GetSize());
    for(int i=0; i<size; i++) {
        contain[i] = another.contain[i];
    }
    return *temp;
}
bool VecSet::operator==(VecSet &another) {
    assert(this -> GetSize() == another.GetSize())
    for(int i=0; i<size; i++) {
        if(contain[i] != another.contain[i])
            return false;
    }
    return true;
}
ostream & VecSet::operator<<(ostream &stream, VecSet &set) {
    for(int i=0; i<size; i++) {
        if(set.IsMember(i)) cout << i <<" ";
    }
    cout << endl;
    return stream;
}

在这里面,+用于求两个集合的并运算,-用于求两个集合的差运算,*用于求两个集合的交运算,=用于把集合another整体赋值给当前集合,==用于判断两个集合是否相等,<<用于输出当前集合。
虽然VecSet的定义与实现都不是很复杂,但还是要注意size的取值,不要取得过大,以防会占用过多的存储空间。

位向量集合具有两个优点:
·由于位向量集合是使用静态数组实现的,因此实现方式简单,算法时间复杂度较低。
·位于位向量集合中的元素与元素之间是没有任何关联的,因此添加或删除某个元素不会对集合中的其他元素造成任何的影响。


单链表集合

虽然前面的位向量集合已经很不错了,但是其最大的一个缺点在于:无法动态的调整整个数组的大小。在之前学习数据结构的时候,我们因为数组是静态的,为了方便我们的动态调整,我们引申出了链表的概念,在这里也完全一样,我们引申出链表集合的概念。
但链表集合的概念又和刚才所说的位向量集合有很大的区别,因为刚才的位向量集合中,我们存放的是二进制的标志值,但在链表集合中,我们的目标是存放各种各样的内容,因此需要达到支持用户自定义数据类型的目的,我们在设计链表集合时采用模板机制。而且我们还需要对><进行重载,以便我们对所定义的内容的大小进行规定,比如对水果的名称进行排序,我们就可以按照字典序的方式进行排列。
首先我们先给出类ListSet的定义:

template <class T> class ListSet;
template <class T>
class ListSetNode
{
    friend class ListSet<T>;
    T data;
    ListSetNode<T>* link;
public:
    ListSetNode(): link(NULL) {}
    ListSetNode(T value): link(NULL), data(value) {}
};
template <class T>
class ListSet
{
    ListSetNode<T>* head;
    ListSetNode<T>* tail;
public:
    ListSet();
    ~ListSet();
    ListSet(ListSet& listSet);
    
    void Add(T add);
    void Del(T del);
    bool IsEmpty();
    void MakeEmpty();
    ListSet<T>& operator+(ListSet<T>& another);
    ListSet<T>& operator-(ListSet<T>& another);
    ListSet<T>& operator*(ListSet<T>& another);
    ListSet<T>& operator=(ListSet<T>& another);
    bool operator==(ListSet<T>& another);
    void Output();
    ListSetNode<T>* GetHead() {return head;}
};

和我们之前所学习的单链表一样,我们需要先定义节点,再定义链表。
接下来我们对每一个成员函数进行实现:

template <class T>
ListSet<T>::ListSet() {
    this->head = new ListSetNode<T>();
    this->tail = this->head;
}
template <class T>
ListSet<T>::~ListSet() {
    MakeEmpty();
    delete head;
}
template <class T>
ListSet<T>::ListSet(ListSet<T>& listSet) {
    this->head = new ListSetNode<T>();
    this->tail = this->head;
    ListSetNode<T>* current = listSet.head->link;
    while(current != nullptr)
    {
        Add(current->data);
        current = current->link;
    }
}

上述构造函数的功能是创建一个只有表头节点的空链表集合。

template <class T>
void ListSet<T>::Add(T value)
{
    ListSetNode<T>* add = new ListSetNode<T>(value);
    ListSetNode<T>* preCurrent = head;   // 记录当前节点
    ListSetNode<T>* current = preCurrent->link;  // 记录当前节点的前驱节点
    while(current != NULL && current->data < value) {   // 寻找插入位置
        preCurrent = current;
        current = preCurrent->link;
    }
    if(head == tail && current == NULL) {   // 向空链表中插入节点
        head -> link = add;
        tail = add;
    }
    if(head != tail && current == NULL) {   // 向链表尾插入值add
        preCurrent -> link = add;
        add -> link = NULL;
        tail = add;
    }
    if(current != NULL && current -> data == value) {   // 链表中已存在值add
        return;
    }
    if(current != NULL && current -> data > value) {
        preCurrent -> link = add;
        add -> link = current;
    }
}

因为我们需要保证集合中元素的唯一性,因此我们在发现链表中已存在值add时,算法直接结束。

template <class T> 
void ListSet<T>::Del(T del) {
    ListSetNode<T>* preCurrent = head;
    ListSetNode<T>* current = preCurrent -> link;
    while(current != NULL && current -> data != del) {   // 寻找删除节点
        preCurrent = current;
        current = preCurrent -> link;
    }
    if(current != NULL) {
        preCurrent -> link = current -> link;
        if(current -> link == NULL) {tail = preCurrent;}   // 更新表尾指针
        delete current;
    }
}

Add(T add)Del(T del)方法与我们之前的链表的基本操作是基本一致的。
但既然是集合,那么就一定需要交,并,差运算,那么我们还是通过运算符重载的方法进行实现。

 /** 并运算 */
 template <class T>
 ListSet<T>& ListSet<T>::operator+(ListSet<T> &another) {
    ListSet<T>* temp = new ListSet(another);
    ListSetNode<T>* current = this -> head -> link;
    while(current != nullptr) {
        temp -> Add(current -> data);
        current = current -> link;
    }
    return *temp;
 }
 /** 差运算 */
 template <class T>
 ListSet<T>& ListSet<T>::operator-(ListSet<T> &another) {
     ListSet<T>* temp = new ListSet(*this);
     ListSetNode<T>* current = another.head -> link;
     while(current != nullptr) {
         temp -> Del(current -> data);
         current = current -> link;
     }
     return *temp;
 }
 /** 交运算 */
 template <class T>
 ListSet<T>& ListSet<T>::operator*(ListSet<T> &another) {
     ListSet<T>* temp = new ListSet(*this);
     *temp = *this - another;
     *temp = *this - *temp;
     return *temp;
 }

在交运算中,我们使用了数学公式:

其余成员函数实现如下:

 template <class T>
 bool ListSet<T>::IsEmpty() {
     return head == tail;
 }
 
 template <class T>
 void ListSet<T>::MakeEmpty() {
     ListSetNode<T>* current = head -> link;
     ListSetNode<T>* del = nullptr;
     while(head -> link != nullptr) {   // 循环删除集合中的元素
         head -> link = current -> link;
         del = current;
         current = current -> link;
         delete del;
     }
     tail = head;
 }
 
 template <class T>
 ListSet<T>& ListSet<T>::operator=(ListSet<T> &another) {
     MakeEmpty();
     ListSetNode<T>* current = another.head -> link;
     while(current != nullptr) {
         Add(current -> data);
         current = current -> link;
     }
     return *this;
 }
 
 template <class T>
 bool ListSet<T>::operator==(ListSet<T> &another) {
     ListSetNode<T>* current1 = head -> link;
     ListSetNode<T>* current2 = another.head -> link;
     while(current1 != nullptr) {
         if(current2 == nullptr) {return false;}
         if(current1 -> data != current2 -> data) {return false;}
         current1 = current1 -> link;
         current2 = current2 -> link;
     }
     if(current2 != nullptr) {return false;}
     return true;
 }
 
 template <class T>
 void ListSet<T>::Output() {
     ListSetNode<T>* current = this -> GetHead() -> link;
     while(current != nullptr) {
         cout << current -> data << " ";
         current = current -> next;
     }
     cout << end;
 }

以上这两种集合表示的方法都具有不同的优点和缺点,因此他们也适用于不同的情况中。因此要根据具体情况进行具体选择。在面临数据量比较大的情况中时,若存储空间允许,类ListSet可以存储“无限”多的数据,类ListSet的缺点是该类中方法的时间复杂度较高,实现效率不如类VecSet


以上就是集合的基本概念与实现方法,下一次我们将继续讲解字典的用法,敬请期待。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • Java集合类可用于存储数量不等的对象,并可以实现常用的数据结构如栈,队列等,Java集合还可以用于保存具有映射关...
    小徐andorid阅读 1,925评论 0 13
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,371评论 0 4
  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,459评论 0 15
  • x战警 第一战 x战警3 背水一战 异星战场 波西杰克逊与神火 波西杰克逊与魔兽 宙斯之子 湮灭 升级
    tao1990_e773阅读 195评论 0 0
  • 关注的很多po主都做了2017年的总结,鉴于我这一年来想做的事情很多但没几件事情做得让自己满意,也就不做作地写总结...
    西比亚阅读 370评论 0 2