数据结构

原著: 数据结构与算法JavaScript描述 原著中代码存在很多问题,本文中的代码都有进行修改

一、列表


列表的抽象数据类型定义

列表是一组有序的数据。每个列表中的数据项称为元素。在 JavaScript 中,列表中的元素可以是任意数据类型。列表中可以保存多少元素并没有事先限定,实际使用时元素的数量受到程序内存的限制。

不包含任何元素的列表称为空列表。列表中包含元素的个数称为列表的 length 。在内部实现上,用一个变量 listSize 保存列表中元素的个数。可以在列表末尾 append 一个元素,也可以在一个给定元素后或列表的起始位置 insert 一个元素。使用 remove 方法从列表中删除元素,使用 clear 方法清空列表中所有的元素。

还可以使用 show() 方法显示列表中所有的元素,使用 getElement() 方法显示当前元素。

列表拥有描述元素位置的属性。列表有前有后(分别对应 front 和 end)。使用 next() 方法可以从当前元素移动到下一个元素,使用 prev() 方法可以移动到当前元素的前一个元素。还可以使用 moveTo(n) 方法直接移动到指定位置,这里的 n 表示要移动到第 n 个位置。currPos 属性表示列表中的当前位置。

列表的抽象数据类型并未指明列表的存储结构,在本章的实现中,我们使用一个数组dataStore 来存储元素

下表:列表的抽象数据类型定义

listSize (属性) 列表的元素个数
pos (属性) 列表的当前位置
length (属性) 返回列表中元素的个数
clear (方法) 清空列表中的所有元素
show (方法) 返回列表的字符串形式
getElement (方法) 返回当前位置的元素
insert (方法) 在现有元素后插入新元素
append (方法) 在列表的末尾添加新元素
remove (方法) 从列表中删除元素
front (方法) 将列表的当前位置设移动到第一个元素
end (方法) 将列表的当前位置移动到最后一个元素
prev (方法) 将当前位置后移一位
next (方法) 将当前位置前移一位
currPos (方法) 返回列表的当前位置
moveTo (方法) 将当前位置移动到指定位置

实现列表类

let List = (() => {
    let listSize = 0,
        dataStore = [];
    pos = 0;
    return class List {
        constructor() {

        }
        clear() {
            dataStore = [];
            listSize = pos = 0;
        }
        find(el) {
            for (let i in dataStore) {
                if (el === dataStore[i]) return i;
            }
            return -1;
        }
        show() {
            return dataStore;
        }
        insert(el, after) {
            let insertPos = this.find(after);
            if (insertPos > -1) {
                listSize += 1;
                dataStore.splice(insertPos, 0, el);
                return true;
            }
            return false;
        }
        append(el) {
            dataStore[listSize++] = el;
        }
        remove(el) {
            let foundAt = this.find(el);
            if (foundAt > -1) {
                dataStore.splice(foundAt, 1);
                listSize -= 1;
                return true;
            }
            return false;
        }
        front() {
            pos = 0;
        }
        end() {
            pos = listSize - 1;
        }
        prev() {
            if (pos > 0) {
                --pos;
            }
        }
        next() {
            if (pos < listSize - 1) {
                ++pos
            }
        }
        length() {
            return listSize;
        }
        currPos() {
            return pos;
        }
        moveTo(p) {
            pos = p;
        }
        getElement() {
            return dataStore[pos];
        }
        contains(el) {
            for (let i in dataStore) {
                if (el === dataStore[i]) return true;
            }
            return false;
        }
    }
})()


let aList = new List();

aList.append('1')
aList.append('2')
aList.append('3')
aList.append('4')
aList.append('5')
aList.append('6')

aList.remove('3')

aList.insert('3', '4')

console.log(aList.contains('2')) // true

console.log(aList.show()) // [ '1', '2', '3', '4', '5', '6' ]

//移动到列表的第一个元素
aList.front();
console.log(aList.getElement()); //1
aList.next();// 第二个元素
aList.next();
aList.next();// 第四个元素
console.log(aList.getElement()); // 4

使用迭代器访问列表

使用迭代器的一些优点。

  • 访问列表元素时不必关心底层的数据存储结构。 •
  • 当为列表添加一个元素时,索引的值就不对了,此时只用更新列表,而不用更新迭代器。 •
  • 可以用不同类型的数据存储方式实现 • cList 类,迭代器为访问列表里的元素提供了一种统一的方式
for(aList.front(); aList.currPos() < aList.length(); aList.next()) {
  console.log(aList.getElement());
}

迭代器只是用来在列表上随意移动,而不应该和任何为列表增加或删除元素的方法一起使用。


二、栈

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

对栈的操作

对栈的两种主要操作是将一个元素压入栈和将一个元素弹出栈。入栈使用 push() 方法,出栈使用 pop() 方法。

另一个常用的操作是预览栈顶的元素。 pop() 方法虽然可以访问栈顶的元素,但是调用该方法后,栈顶元素也从栈中被永久性地删除了。 peek() 方法则只返回栈顶元素,而不删除它。

为了记录栈顶元素的位置,同时也为了标记哪里可以加入新元素,我们使用变量 top ,当向栈内压入元素时,该变量增大;从栈内弹出元素时,该变量减小。

push() 、 pop() 和 peek() 是栈的 3 个主要方法,但是栈还有其他方法和属性。 clear() 方法清除栈内所有元素, length 属性记录栈内元素的个数。我们还定义了一个 empty 属性,用以表示栈内是否含有元素,不过使用 length 属性也可以达到同样的目的。

栈的实现

let Stack = (() => {
    let dataStore = [],
        top = 0;
    return class Stack {
        push(el) {
            dataStore[top++] = el;
        }
        pop() { // 它返回栈顶元素,同时将变量 top 的值减 1
            if (top <= 0) {
                return '出栈失败,空栈顶'
            }
            return dataStore[--top];
        }
        peek() { // 返回数组的第 top-1 个位置的元素,即栈顶元素
            return dataStore[top - 1];
        }
        length() {
            return top;
        }
        clear() {
            top = 0;
        }
    }
})()

let aStack = new Stack();

aStack.push('栈1');
aStack.push('栈2');
aStack.push('栈3');
aStack.push('栈4');

console.log(aStack.peek())
aStack.pop();
aStack.pop();
aStack.pop();
console.log(aStack.peek())

使用栈

将数字转换为二进制和八进制

function mulBase(num, base) {
    let s = new Stack();
    do {
        s.push(num % base);
        num = Math.floor(num / base);
    } while (num > 0);

    let converted = "";

    while(s.length() > 0){
        converted += s.pop();
    }
    return converted;
}

console.log(mulBase(21, 2)); // 10101

回文

function isPalindrome(word) {
    let s = new Stack();
    for (let i in word) {
        s.push(word[i]);
    }

    var result = "";

    while(s.length() > 0) {
        result += s.pop()
    }
    return result === word;
}

console.log(isPalindrome("ht"))

三、队列

队列是一种列表,不同的是队列只能在队尾插入元素,在队首删除元素。队列用于存储按顺序排列的数据,先进先出

对队列的操作

队列的两种主要操作是:向队列中插入新元素和删除队列中的元素。插入操作也叫做入队,删除操作也叫做出队。入队操作在队尾插入新元素,出队操作删除队头的元素。

队列的另外一项重要操作是读取队头的元素。这个操作叫做 peek() 。我们还想知道队列中存储了多少元素,可以使用 length 属性满足该需求;要想清空队列中的所有元素,可以使用 clear() 方法来实现。

队列的实现

let Queue = (() => {
    let dataStore = [];

    return class Queue {
        enqueue(el) { // 入队
            dataStore.push(el)
        }
        dequeue() { // 出队
            return dataStore.shift();
        }
        front(){
            return dataStore[0];
        }
        back(){
            return dataStore[dataStore.length-1];
        }
        show(){
            return dataStore;
        }
    }
})()


aQueue.enqueue('1')
aQueue.enqueue('2')
aQueue.enqueue('3')
aQueue.enqueue('4')

console.log(aQueue.show()) // [ '1', '2', '3', '4' ]
aQueue.dequeue();
console.log(aQueue.show()) // [ '2', '3', '4' ]

优先队列

在一般情况下,从队列中删除的元素,一定是率先入队的元素。但是也有一些使用队列的应用,在删除元素时不必遵守先进先出的约定。这种应用,需要使用一个叫做优先队列的数据结构来进行模拟。

从优先队列中删除元素时,需要考虑优先权的限制。比如医院急诊科(EmergencyDepartment)的候诊室,就是一个采取优先队列的例子。当病人进入候诊室时,分诊护士会评估患者病情的严重程度,然后给一个优先级代码。高优先级的患者先于低优先级的患者就医,同样优先级的患者按照先来先服务的顺序就医。

class Patient {
    constructor(name, code) {
        this.name = name;
        this.code = code; // 病人严重程度
    }
}

let aqueue = new Queue();

let aP1 = aqueue.enqueue(new Patient('张三', 3));
let aP2 = aqueue.enqueue(new Patient('李四', 2));
let aP3 = aqueue.enqueue(new Patient('王五', 1));

console.log(aqueue.show()) // 张三3,李四2,王五1,

console.log(aqueue.dequeue()) // [ Patient { name: '王五', code: 1 } ]

四、链表

数组不总是组织数据的最佳数据结构,原因如下。在很多编程语言中,数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难。在数组中,添加和删除元素也很麻烦,因为需要将数组中的其他元素向前或向后平移,以反映数组刚刚进行了添加或删除操作。然而,JavaScript 的数组并不存在上述问题,因为使用 split() 方法不需要再访问数组中的其他元素了。

JavaScript 中数组的主要问题是,它们被实现成了对象,与其他语言(比如 C++ 和 Java)的数组相比,效率很低

如果你发现数组在实际使用时很慢,就可以考虑使用链表来替代它。除了对数据的随机访问,链表几乎可以用在任何可以使用一维数组的情况中。如果需要随机访问,数组仍然是更好的选择。

定义链表

链表是由一组节点组成的集合。每个节点都使用一个对象的引用指向它的后继。指向另一个节点的引用叫做链,链表的尾元素指向一个 null 节点

链表中插入一个节点的效率很高。向链表中插入一个节点,需要修改它前面的节点(前驱),使其指向新加入的节点,而新加入的节点则指向原来前驱指向的节点。

从链表中删除一个元素也很简单。将待删除元素的前驱节点指向待删除元素的后继节点,同时将待删除元素指向 null ,元素就删除成功了。图 下 演示了从链表中删除“bacon”的过程。

设计一个基于对象的链表

Node 类用来表示节点, LinkedList 类提供了插入节点、删除节点、显示列表元素的方法,以及其他一些辅助方法。

head 节点的 next 属性被初始化为 null ,当有新元素插入时, next 会指向新的元素

插入新的节点

在一个已知的节点后面加入新元素时,先要找到后面的新节点。为此创建一个find来查找。

一旦找到“后面”的节点,就可以将新节点插入链表了。首先,将新节点的 next 属性设置为“后面”节点的 next 属性对应的值。然后设置“后面”节点的 next 属性指向新节点。就可以用inser方法加入

通过画图来让大家容易理解一点

之后定义一个 display() 方法,该方法用来显示链表中的元素

最后定义一个remove方法来删除连边中的元素。从链表中删除节点时,需要先找到待删除节点前面的节点。找到这个节点后,修改它的next 属性,使其不再指向待删除节点,而是指向待删除节点的下一个节点。定义findPrevious方法来查找待删除元素之前的元素。

class Node {
    constructor(el) {
        this.element = el;
        this.next = null;
    }
}

let LinkedList = (() => {

    let head = new Node("head");
    return class LinkedList {
        find(item){
          let currNode = head;
          while (currNode.element != item) {
            currNode = currNode.next;
          }
          return currNode;
        }
        findPrevious(item) {
          let currNode = head;
          while ((currNode.next != null) && (currNode.next.element != item)) {
            currNode = currNode.next;
          }
          return currNode;
        }
        insert(el, item){
          let newNode = new Node(el);
          let currNode = this.find(item);
          newNode.next = currNode.next;
          currNode.next = newNode; 
        }
        remove(item){
          let currNode = this.findPrevious(item);
          let preNode = this.find(item);
          currNode.next = preNode.next;
        }
        display(){
          let node = head;
          while( node.next != null) {
            console.log(node.next.element);
            node = node.next;
          }
        }
    }
})()

let aList =  new LinkedList();

aList.insert("1", "head")
aList.insert("2", "1")
aList.insert("3", "2")

aList.display(); // 1 2 3

aList.remove('3')

aList.display();// 1 2

双向链表

尽管从链表的头节点遍历到尾节点很简单,但反过来,从后向前遍历则没那么简单。通过给 Node 对象增加一个属性,该属性存储指向前驱节点的链接,这样就容易多了。

首当其冲的是要为 Node 类增加一个 previous 属性

class Node {
    constructor(el) {
        this.element = el;
        this.next = null;
        this.previous = null;
    }
}

insert() 方法延申,需要设置新节点的 previous 属性

insert(el, item) {
    let newNode = new Node(el);
    let currNode = this.find(item);
    newNode.previous = currNode;
    newNode.next = currNode.next;
    currNode.next = newNode;
}

remove 方法 就不用查找前一个节点了

remove(item) {
    let currNode = this.find(item);
    if (currNode.next == null) {
        currNode.previous.next = null
        currNode.previous = null;
    } else {
        currNode.next.previous = currNode.previous;
        currNode.previous.next = currNode.next;
        currNode.next = null;
        currNode.previous = null;
    }
}

dispReverse() 方法 反序显示双向链表中的元素

dispReverse() {
    let currNode = head;
    currNode = this.findLast();
    while (!(currNode.previous == null)) {
        console.log(currNode.element);
        currNode = currNode.previous;
    }
}

循环链表

循环链表和单向链表相似,节点类型都是一样的。唯一的区别是,在创建循环链表时,让其头节点的 next 属性指向它本身

如果你希望可以从后向前遍历链表,但是又不想付出额外代价来创建一个双向链表,那么就需要使用循环链表。从循环链表的尾节点向后移动,就等于从后向前遍历链表

创建循环链表,只需要修改 LList 类的构造函数

let LinkedList = (() => {
    let head = new Node("head");
        head.next = head;
    return class LinkedList {
      //...
    }
  }
)()

只需要修改一处,就将单向链表变成了循环链表。但是其他一些方法需要修改才能工作正常,之前判断.next != null 的都需要稍加修改


5、字典

字典是一种以键 - 值对形式存储数据的数据结构,就像电话号码簿里的名字和电话号码一样。要找一个电话时,先找名字,名字找到了,紧挨着它的电话号码也就找到了。这里的键是指你用来查找的东西,值是查找得到的结果。

javascript Object类就是以字典的形式来设计的。

字典类

字典类的基础是 Array 类,而不是 Object 类。

从字典中删除键 - 值对需要使用 JavaScript 中的一个内置函数: delete 。

class Dictionary {
    constructor() {
        this.dataStore = new Array()
    }

    add(key, val) {
        this.dataStore[key] = val;
    }

    find(key) {
        return this.dataStore[key]
    }

    remove(key) {
        delete this.dataStore[key]
    }

    show() {
        console.log(this.dataStore)
        for (let i in this.dataStore) {
            console.log(`key : ${i} ===> value : ${this.dataStore[i]}`)
        }
    }
}

let aD = new Dictionary();

aD.add('name', "hongtao")
aD.add('age', "22")

aD.remove('name')

aD.show(); // key : age ===> value : 22

拓展

查看字典里元素的个数

length() {
    return Object.keys(this.dataStore).length;
}

清空

clear() {
    this.dataStore = new Array()
}

排序

Object.keys(this.datastore).sort()

散列(hash)

散列是一种常用的数据存储技术,散列后的数据可以快速地插入或取用。

下图 以一个小型电话号码簿为例,阐释了散列的概念

定义散列类

class HashTable {
    constructor() {
        this.table = new Array(137);
    }
}

选择一个散列函数

散列函数的选择依赖于键值的数据类型,如果键是整型,最简单的散列函数就是以数组的长度对键取余。在一些情况下,比如数组的长度是 10,而键值都是 10 的倍数时,就不推荐使用这种方式了。这也是数组的长度为什么要是质数的原因之一,就像我们在上个构造函数中,设定数组长度为 137 一样。如果键是随机的整数,则散列函数应该更均匀地分布这些键。这种散列方式称为除留余数法

将字符串中每个字符的 ASCII 码值相加似乎是一个不错的散列函数。这样散列值就是 ASCII 码值的和除以数组长度的余数。

put() 和 showDistro() ,一个用来将数据存入散列表,一个用来显示散列表中的数据

class HashTable {
    constructor() {
        this.table = new Array(137);
    }

    simpleHash(data) {
        let total = 0;
        for (let i = 0; i < data.length; i++) {
            total += data.charCodeAt(i);
        }

        return total % this.table.length;
    }
    put(data) {
        let pos = this.simpleHash(data);
        this.table[pos] = data;
    }

    showDistro() {
        for (let i in this.table) {
            if(this.table[i] != undefined) {
                console.log(`${i} : ${this.table[i]}`);
            }
        }
    }
}

let ht = new HashTable();

let names = ["David", "Jennifer", "Donnie", "Raymond","Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];

for(let i in names) {
    ht.put(names[i]);
}

ht.showDistro();
/*
35 : Cynthia
45 : Clayton
57 : Donnie
77 : David
95 : Danny
116 : Mike
132 : Jennifer
134 : Jonathan
*/

很显然,数据并不是均匀分布的,人名向数组的两端集中。而且上面打印出来的只有8个,可是我们的names里有9个名字,这就是碰撞的问题,什么时候发生了碰撞呢。往下看

simpleHash(data) {
    let total = 0;
    for (let i = 0; i < data.length; i++) {
        total += data.charCodeAt(i);
    }
    console.log('hash 列表 :' + data + "---->" + total)
    return total % this.table.length;
}

// 打印结果

/*
hash 列表 :David---->488
hash 列表 :Jennifer---->817
hash 列表 :Donnie---->605
hash 列表 :Raymond---->730
hash 列表 :Cynthia---->720
hash 列表 :Mike---->390
hash 列表 :Clayton---->730
hash 列表 :Danny---->506
hash 列表 :Jonathan---->819
*/

我们发现 经过hash算法转换过的 Clayton 和 Raymond 都是730 这就发生了碰撞的问题,所以Clayton会将 Raymond给覆盖了。

一个更好的散列函数

为了避免碰撞,首先要确保散列表中用来存储数据的数组其大小是个质数。这一点很关键,这和计算散列值时使用的取余运算有关。

为了避免碰撞,在给散列表一个合适的大小后,接下来要有一个计算散列值的更好方法。霍纳算法很好地解决了这个问题。在此算法中,新的散列函数仍然先计算字符串中各字符的 ASCII 码值,不过求和时每次要乘以一个质数。大多数算法书建议使用一个较小的质数

betterHash(string) {
    const H = 37;
    var total = 0;
    for (var i = 0; i < string.length; ++i) {
        total += H * total + string.charCodeAt(i);
    }
    total = total % this.table.length;
    if (total < 0) {
        total += this.table.length - 1;
    }
    return parseInt(total);
}

这次所有的人名都显示出来了,而且没有碰撞

散列化整型键

此处介绍如何散列化整型键,使用的数据集是学生的成绩。我们将随机产生一个 9 位数的键,用以识别学生身份和一门成绩。下面是产生学生数据(包含 ID 和成绩)的函数:

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function genStuData(arr) {
    for (var i = 0; i < arr.length; ++i) {
        var num = "";
        for (var j = 1; j <= 9; ++j) {
            num += Math.floor(Math.random() * 10);
        }
        num += getRandomInt(50, 100);
        arr[i] = num;
    }
}

使用 getRandomInt() 函数时,可以指定随机数的最大值和最小值。拿学生的成绩来说,最低分是 50,最高分是 100。

genStuData() 函数生成学生的数据。里层的循环用来生成学生的 ID,紧跟在循环后面的代码生成一个随机的成绩,并把成绩缀在 ID 的后面。主程序会把 ID 和成绩分离。散列函数将学生 ID 里的数字相加,使用 simpleHash() 函数计算出散列值。

完整代码

class HashTable {
    constructor() {
        this.table = new Array(137);
    }

    simpleHash(data) {
        var total = 0;
        for (var i = 0; i < data.length; ++i) {
            total += data.charCodeAt(i);
        }
        console.log("Hash value: " + data + " -> " + total);
        return total % this.table.length;
    }

    betterHash(string) {
        const H = 37;
        var total = 0;
        for (var i = 0; i < string.length; ++i) {
            total += H * total + string.charCodeAt(i);
        }
        total = total % this.table.length;
        if (total < 0) {
            total += this.table.length - 1;
        }
        return parseInt(total);
    }
    put(data) {
        let pos = this.betterHash(data);
        this.table[pos] = data;
    }

    showDistro() {
        for (let i in this.table) {
            if (this.table[i] != undefined) {
                console.log(`${i} : ${this.table[i]}`);
            }
        }
    }
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function genStuData(arr) {
    for (var i = 0; i < arr.length; ++i) {
        var num = "";
        for (var j = 1; j <= 9; ++j) {
            num += Math.floor(Math.random() * 10);
        }
        num += getRandomInt(50, 100);
        arr[i] = num;
    }
}
var numStudents = 10;
var arrSize = 97;
var idLen = 9;
var students = new Array(numStudents);

genStuData(students);

console.log("Student data: \n");

for (var i = 0; i < students.length; ++i) {
    console.log(students[i].substring(0, 8) + " " +
        students[i].substring(9));
}


console.log("\n\nData distribution: \n");

var hTable = new HashTable();

for (var i = 0; i < students.length; ++i) {
    hTable.put(students[i]);
}

对散列表排序、从散列表中取值

class HashTable {
    constructor() {
        this.table = new Array(137);
    }

    betterHash(string) {
        const H = 37;
        var total = 0;
        for (var i = 0; i < string.length; ++i) {
            total += H * total + string.charCodeAt(i);
        }
        total = total % this.table.length;
        if (total < 0) {
            total += this.table.length - 1;
        }
        return parseInt(total);
    }
    put(data) {
        let pos = this.simpleHash(data);
        this.table[pos] = data;
    }

    showDistro() {
        for (let i in this.table) {
            if (this.table[i] != undefined) {
                console.log(`${i} : ${this.table[i]}`);
            }
        }
    }
}

定义 get() 方法,用以读取存储在散列表中的数据。该方法同样需要对键值进行散列化,然后才能知道数据到底存储在数组的什么位置。

get(key){
    return this.table[this.betterHash(key)]
}

碰撞处理

当散列函数对于多个输入产生同样的输出时,就产生了碰撞。散列算法的第二部分就将介绍如何解决碰撞,使所有的键都得以存储在散列表中。本节将讨论两种碰撞解决办法:开链法线性探测法

开链法

当碰撞发生时,我们仍然希望将键存储到通过散列算法产生的索引位置上,但实际上,不可能将多份数据存储到一个数组单元中。开链法是指实现散列表的底层数组中,每个数组元素又是一个新的数据结构,比如另一个数组,这样就能存储多个键了。使用这种技术,即使两个键散列后的值相同,依然被保存在同样的位置,只不过它们在第二个数组中的位置不一样罢了。如下图

线性探测法

第二种处理碰撞的方法是线性探测法。线性探测法隶属于一种更一般化的散列技术:开放寻址散列。当发生碰撞时,线性探测法检查散列表中的下一个位置是否为空。如果为空,就将数据存入该位置;如果不为空,则继续检查下一个位置,直到找到一个空的位置为止。该技术是基于这样一个事实:每个散列表都会有很多空的单元格,可以使用它们来存储数据。

修改put方法:

put(data) {
    let pos = this.betterHash(data);
    let index = 0;
    if (this.table[pos] == undefined) {
        this.table[pos] = data;
    } else {
        while (this.table[++pos] != undefined) {}
        this.table[pos] = data;
    }
}

当存储数据使用的数组特别大时,选择线性探测法要比开链法好。这里有一个公式,常常可以帮助我们选择使用哪种碰撞解决办法:如果数组的大小是待存储数据个数的 1.5 倍,那么使用开链法;如果数组的大小是待存储数据的两倍及两倍以上时,那么使用线性探测法。


集合

集合(set)是一种包含不同元素的数据结构。集合中的元素称为成员。集合的两个最重要特性是:首先,集合中的成员是无序的;其次,集合中不允许相同成员存在。

当你想要创建一个数据结构,用来保存一些独一无二的元素时,比如一段文本中用到的单词,集合就变得非常有用。

集合的定义

  • 空集,全集则是包含一切可能成员的集合。
  • 如果两个集合的成员完全相同,则称两个集合相等。
  • 如果一个集合中所有的成员都属于另外一个集合,则前一集合称为后一集合的 子集。

对集合的操作

  • 并集 : 将两个集合中的成员进行合并,得到一个新集合。
  • 交集 : 两个集合中共同存在的成员组成一个新的集合。
  • 补集 : 属于一个集合而不属于另一个集合的成员组成的集合。

Set 类的实现

Set类 中有

add()方法:我们添加一个data进入集合,如果集合中没有data则添加成功,如果有,则添加失败。

remove方法:删除集合中的data,如果集合中有data就删除,返回ture,否则返回ture。

union()方法:执行并集操作。返回新的集合

intersect()方法:交集 ∩,返回新的集合

subset()方法: 该集合是否是比较集合的子集。 首先要确定该集合的长度是否小于待比较集合。如果该集合比待比较集合还要大,那么该集合肯定不会是待比较集合的一个子集。当该集合的长度小于待比较集合时,再判断该集合内的成员是否都属于待比较集合。如果有任意一个成员不属于待比较集合,则返回 false ,程序终止。如果一直比较完该集合的最后一个元素,所有元素都属于待比较集合,那么该集合就是待比较集合的一个子集,该方法返回 true 。

difference()方法:补集。该方法返回一个新集合,该集合包含的是那些属于第一个集合但不属于第二个集合的成员。

class Set {
    constructor() {
        this.dataStore = [];
    }
    show() {
        return this.dataStore;
    }
    add(data) {
        if (this.dataStore.indexOf(data) < 0) {
            this.dataStore.push(data);
            return true;
        }
        return false;
    }
    remove(data) {
        let pos = this.dataStore.indexOf(data);
        if (pos < 0) {
            this.dataStore.splice(pos, 1);
            return true;
        }
        return false;
    }
    contains(data) {
        if (this.dataStore.indexOf(data) > -1) {
            return true
        }
        return false
    }
    union(set) {
        let tempSet = new Set();
        for (let i in this.dataStore) {
            tempSet.add(this.dataStore[i]);
        }
        for (let i in set.dataStore) {
            if (!tempSet.contains(set.dataStore[i])) {
                tempSet.dataStore.push(set.dataStore[i]);
            }
        }
        return tempSet;
    }
    intersect(set) {
        let tempSet = new Set();
        for (let i in set.dataStore) {
            if (this.dataStore.indexOf(set.dataStore[i]) > -1) {
                tempSet.add(set.dataStore[i])
            }
        }
        return tempSet;
    }
    size() {
        return this.dataStore.length;
    }
    subset(set) {
        if (this.size() > set.size()) {
            return false;
        } else {
            for (let i in this.dataStore) {
                if (!set.contains(this.dataStore[i])) {
                    return false;
                }
            }
            return true;
        }
    }
    difference(set){
        let tempSet = new Set();
        for(let i in this.dataStore) {
            if(set.dataStore.indexOf(this.dataStore[i]) < 0) {
                tempSet.add(this.dataStore[i])
            }
        }
        return tempSet;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容