《数据结构与算法-JavaScript描述》--数据结构

ps:最近想学英语,如果哪位朋友有全英文书籍想要卖(最好难度不大@-@),可以私聊我,收购。

----------超长文预警,需要花费大量时间----------

今天整理《数据结构与算法-JavaScript描述》中所讲的数据结构。

1、数组

关于数组,之前整理过思维导图,感觉比书上的全。
数组.png

2、列表

在日常生活中,人们经常使用列表:待办事项列表、购物清单、十佳榜单、最后十名榜单 等。计算机程序也在使用列表,尤其是列表中保存的元素不是太多时。当不需要在一个很 长的序列中查找元素,或者对其进行排序时,列表显得尤为有用。反之,如果数据结构非 常复杂,列表的作用就没有那么大了。

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

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

JavaScript实现列表

    // 列表
    function List(){
            this.listSize = 0; // 列表的元素个数
            this.pos = 0; //列表的当前位置
            this.dataStore = [];//初始化一个空数组来保存列表元素
            // length:列表中有多少个元素
            this.length = function(){
                return this.listSize;
            };
            // clear:清空列表中所有的元素
            this.clear = function(){
                delete this.dataStore;
                this.dataStore = [];
                this.listSize = this.pos = 0;
            };
            // 判断元素是否在当前列表中,如果在返回元素下标,不在返回-1
            this.find = function(element){
                for(var i = 0 ; i < this.dataStore.length ; i++){
                    if(this.dataStore[i] == element){
                        return i;
                    }
                }
                return -1;
            };
            // 显示列表中的元素
            this.toString = function(){
                return this.dataStore;
            };
            // insert:在element元素后面插入after元素
            this.insert = function(element,after){
                var insertPos = this.find(after);
                if(insertPos > -1){
                    this.dataStore.splice(insertPos+1,0,element);
                    this.listSize++;
                    return true;
                }
                return false;
            };
            // append:在列表末尾插入一个元素
            this.append = function(element){
                this.dataStore[this.listSize++] = element;
            };
            // remove:从列表中删除元素
            this.remove = function(element){
                var foundAt = this.find(element);
                if (foundAt > -1){
                    this.dataStore.splice(foundAt,1)
                    this.listSize--;
                    return true;
                }
                return false;
            };
            // front:将列表的当前位置设移动到第一个元素
            this.front = function(){
                return this.pos = 0;
            };
            // getElement:返回当前位置的元素
            this.getElement = function() {
                return this.dataStore[this.pos]
            };
            // contains:判断给定值是否在列表中
            this.contains = function(element){
                for (var i = 0 ; i < this.dataStore.length ; i++){
                    if(this.dataStore[i] == element){
                        return true;
                    }
                }
                return false
            };
            // 最后一组方法允许用户在列表上自由移动
            // 将列表的当前位置移动到最后一个元素
            this.end = function(){
                return this.pos = this.listSize-1;
            };
            // 将当前位置后移一位
            this.prev = function(){
                if(this.pos>0){
                    return this.pos--;
                }
            };
            // 将当前位置前移一位
            this.next = function(){
                if(this.pos < this.listSize-1){
                    return ++this.pos;
                }else{
                    return this.pos = this.listSize;
                }
            };
            // 返回列表的当前位置
            this.currPos = function(){
                return this.pos;
            };
            // 将当前位置移动到指定位置
            this.moveTo = function(position){
                return this.pos = position;
            };
        }

// 测试
    var names = new List();
    names.append("Clayton");
    names.append("Raymond");
    names.append("Cynthia");
    names.append("Jennifer");
    names.append("Bryan");
    names.append("Danny");

    console.log(names.toString()) 
    //["Clayton", "Raymond", "Cynthia", "Jennifer", "Bryan", "Danny"]   
    console.log(names.listSize) //6 
    console.log(names.find('Danny')) // 5
    console.log(names.insert('Luckfine','Jennifer')) // true
    console.log(names.toString()) 
    //["Clayton", "Raymond", "Cynthia", "Jennifer", "Luckfine", "Bryan", "Danny"]
    console.log(names.listSize) //7
    console.log(names.remove('Luckfine')) //true
    console.log(names.toString()) 
    //["Clayton", "Raymond", "Cynthia", "Jennifer", "Bryan", "Danny"]
    console.log(names.listSize) //6

    console.log(names.front()) // 0
    console.log(names.getElement()) // Clayton    
    console.log(names.next())// 1
    console.log(names.getElement()) //Raymond

    console.log(names.moveTo(4))
    console.log(names.getElement())//Bryan
    console.log(names.currPos()) //4

    console.log(names.end()) //5
    console.log(names.getElement()) //Danny

小案例:使用列表管理实现影碟租赁

// 使用列表管理影碟租赁
var movies = ['《肖申克的救赎》','《教父》','《低俗小说》','《黄金三镖客','《十二怒汉》','《辛德勒名单》','《黑暗骑士》','《指环王:王者归来》','《搏击俱乐部》','《星球大战 5:帝国反击战》','《飞越疯人院》','《指环王:护戒使者》','《盗梦空间》','《好家伙》','《星球大战》','《黑客帝国》','《阿甘正传》','《上帝之城》']

var movieList = new List();
var customers = new List();

for (var i = 0; i < movies.length; ++i) {
    movieList.append(movies[i]);
}

// displayList函数来显示影碟店里现有的影碟清单
function displayList(list) {
    for (list.front(); list.currPos() < list.length(); list.next()) {
        // 如果传进来的是顾客借阅的书,那就显示借阅者和借阅的影碟
        if (list.getElement() instanceof Customer) {
            console.log(list.getElement()["name"] + ", " +
                list.getElement()["movie"]);
        } else {
            console.log(list.getElement());
        }
    }
}
// 用来存已经借阅的客户和影碟
function Customer(name, movie) {
    this.name = name;
    this.movie = movie;
}
// 借阅影碟
function checkOut(name, movie, filmList, customerList) {
    if (movieList.contains(movie)) {
        var c = new Customer(name, movie); 
        customerList.append(c); 
        filmList.remove(movie);
    } else {
        console.log(movie + " is not available.");
    }
}
// 归还影碟
function checkIn(name, movie, filmList, customerList) {
    // console.log([name, movie])
    for (customers.front(); customers.currPos() < customers.length(); customers.next()) {
        // 在借阅列表中判断有无归还的影碟,如果有,将其移除
        if(customers.getElement()["name"]==name && customers.getElement()["movie"]==movie){
            customers.remove(customers.getElement())
            filmList.append(movie);
        }else{
            console.log(movie + " is not available.");
        }
    }
}

console.log(checkOut('Luckfine','《星球大战》',movieList,customers))
console.log(checkOut('Luckfine','《肖申克的救赎》',movieList,customers))
console.log(checkIn('Luckfine','《肖申克的救赎》',movieList,customers))
console.log(customers.toString())
console.log(displayList(customers))

3、栈

栈是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶。咖啡厅内 的一摞盘子是现实世界中常见的栈的例子。只能从最上面取盘子,盘子洗净后,也只能摞 在这一摞盘子的最上面。栈被称为一种后入先出(LIFO,last-in-first-out)的数据结构。

由于栈具有后入先出的特点,所以任何不在栈顶的元素都无法访问。为了得到栈底的元 素,必须先拿掉上面的元素。

《数据结构与算法-JavaScript描述》--数据结构_第1张图片

JavaScript实现栈

    function Stack() {
        this.dataStore = []; // 保存栈内元素
        this.top = 0; //变量top记录栈顶位置 初始化为0
        //向栈内压入新元素
        this.push = function(element){
            return this.dataStore[this.top++] = element;
        }; 
        //它返回栈顶元素,同时将变量 top 的值减 1
        this.pop = function(){
            return this.dataStore[--this.top];
        };
        //返回数组的第 top-1 个位置的元素,即栈顶元素
        this.peek = function(){
            return this.dataStore[this.top-1];
        }; 
        //clear() 方法 清除栈内所有元素
        this.clear = function(){
            return this.top = 0;
        };
        //length 属性记录栈内元素的个数
        this.length = function(){
            return this.top;
        };
    }

    // 测试stack类的实现
    var s = new Stack();
    s.push("David");
    s.push("Raymond");
    s.push("Bryan");
    console.log(s.length())
    console.log(s.peek());
    console.log(s.pop())
    console.log(s.length())
    s.clear();
    console.log(s.length());


    // 判定给定的字符串是否是回文
    function isPalindrome(word) {
        var s = new Stack();
        for (var i = 0; i < word.length; ++i) {
            s.push(word[i]);
        }
        var rword = "";
        while (s.length() > 0) {
            rword += s.pop();
        }
        if (word == rword) {
            return true;
        }else {
            return false;
        }
    }

    console.log(isPalindrome('hello')); //false 
    console.log(isPalindrome('helleh')); //true

4、队列

队列是一种列表,不同的是队列只能在队尾插入元素,在队首删除元素。队列用于存储按 顺序排列的数据,先进先出,这点和栈不一样,在栈中,最后入栈的元素反而被优先处 理。可以将队列想象成在银行前排队的人群,排在最前面的人第一个办理业务,新来的人 只能在后面排队,直到轮到他们为止。

队列是一种先进先出(First-In-First-Out,FIFO)的数据结构。队列被用在很多地方,比如 提交操作系统执行的一系列进程、打印任务池等,一些仿真系统用队列来模拟银行或杂货 店里排队的顾客。

《数据结构与算法-JavaScript描述》--数据结构_第2张图片

JavaScript实现队列

    // 使用数组来实现队列看起来顺理成章。JavaScript 中的数组具有其他编程语言中没有的优点, 数组的 push() 方法可以在数组末尾加入元素,shift() 方法则可删除数组的第一个元素。

    function Queue() {
        this.dataStore = [];
        // enqueue() 方法向队尾添加一个元素:
        this.enqueue = function(element){
            return this.dataStore.push(element);
        };
        // dequeue() 方法删除队首的元素
        this.dequeue = function(){
            return this.dataStore.shift();
        };
        // 读取队首
        this.front = function(){
            return this.dataStore[0];
        };
        // 读取队尾
        this.back = function(){
            return this.dataStore[this.dataStore.length-1];
        };
        // toString() 方法显示队列内的所有元素
        this.toString = function(){
            var retStr = "";
            for (var i = 0; i < this.dataStore.length; ++i) {
                retStr += this.dataStore[i] + "\n";
            }
            return retStr;
        };
        // 判断队列是否为空:
        this.empty = function(){
            if (this.dataStore.length == 0) {
                return true;
            } else {
                return false;
            }
        };
    }   
    var q = new Queue();
    q.enqueue("Meredith");
    q.enqueue("Cynthia");
    q.enqueue("Jennifer");
    console.log(q.toString())//Meredith Cynthia Jennifer
    q.dequeue();
    console.log(q.toString())//Cynthia Jennifer
    console.log(q.front())//Cynthia
    console.log(q.back())//Jennifer
    console.log(q.enqueue('Luckfine'))
    console.log(q.toString()) //Cynthia Jennifer Luckfine

5、链表

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

《数据结构与算法-JavaScript描述》--数据结构_第3张图片

数组元素靠它们的位置进行引用,链表元素则是靠相互之间的关系进行引用。在图 6-1 中, 我们说 bread 跟在 milk 后面,而不说 bread 是链表中的第二个元素。遍历链表,就是跟着 链接,从链表的首元素一直走到尾元素(但这不包含链表的头节点,头节点常常用来作为 链表的接入点)。图中另外一个值得注意的地方是,链表的尾元素指向一个 null 节点。

链表中插入一个节点的效率很高。向链表中插入一个节点,需要修改它前面的节点(前 驱),使其指向新加入的节点,而新加入的节点则指向原来前驱指向的节点。图 6-3 演示了 如何在 eggs 后加入 cookies。

《数据结构与算法-JavaScript描述》--数据结构_第4张图片

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

《数据结构与算法-JavaScript描述》--数据结构_第5张图片

JavaScript实现链表

    function Node(element) {
        this.element = element;//element 用来保存节点上的数据
        this.next = null;//next 用来保存指向下一个节点的链接
    }
    function LList() {
        this.head = new Node("head"); 
        // 遍历链表,查找给定数据。如果找到数据,该方法就返回保存该数据的节点。
        // find() 方法演示了如何在链表上进行移动。首先,创建一个新节点,并将链表的头节点赋 给这个新创建的节点。然后在链表上进行循环,如果当前节点的 element 属性和我们要找 的信息不符,就从当前节点移动到下一个节点。如果查找成功,该方法返回包含该数据的 节点;否则,返回 null。
        this.find = function(item){
            var currNode = this.head;
            while (currNode.element != item) {
               currNode = currNode.next;
            }
            return currNode;
        };
        //  insert():向链表中插入一个节点
        // 该方法向链表中插入一个节点。向链表中插入新节点 时,需要明确指出要在哪个节点前面或后面插入。一旦找到“后面”的节点,就可以将新节点插入链表了。首先,将新节点的 next 属性设 置为“后面”节点的 next 属性对应的值。然后设置“后面”节点的 next 属性指向新节点。
        this.insert = function(newElement, item){
            var newNode = new Node(newElement); 
            var current = this.find(item); 
            newNode.next = current.next; 
            current.next = newNode;
        }; 
        // 从链表中删除节点时,需要先找到待删除节点前面的节点。找到这个节点后,修改它的 next 属性,使其不再指向待删除节点,而是指向待删除节点的下一个节点。
        this.remove = function(item){
            var prevNode = this.findPrevious(item);
            if (!(prevNode.next == null)) {
                prevNode.next = prevNode.next.next;
            }
        }; 
        this.display = function(){
            var currNode = this.head;
            while (!(currNode.next == null)) {
               console.log(currNode.next.element);
               currNode = currNode.next;
            }
        };
        this.findPrevious = function(item){
            var currNode = this.head;
            while (!(currNode.next == null) && (currNode.next.element != item)) {
                currNode = currNode.next;
            }
            return currNode;        
        }
    }
    var cities = new LList();
    cities.insert("Conway", "head");
    cities.insert("Russellville", "Conway");
    cities.insert("Alma", "Russellville");
    cities.display()//Conway  Russellville  Alma
    cities.remove("Carlisle")
    cities.display();//Russellville  Alma

双向链表

尽管从链表的头节点遍历到尾节点很简单,但反过来,从后向前遍历则没那么简单。通过 给 Node 对象增加一个属性,该属性存储指向前驱节点的链接,这样就容易多了。此时向链 表插入一个节点需要更多的工作,我们需要指出该节点正确的前驱和后继。但是在从链表 中删除节点时,效率提高了,不需要再查找待删除节点的前驱节点了。图 6-5 演示了双向 链表的工作原理。

《数据结构与算法-JavaScript描述》--数据结构_第6张图片

双向链表的 remove() 方法比单向链表的效率更高,因为不需要再查找前驱节点了。首先需 要在链表中找出存储待删除数据的节点,然后设置该节点前驱的 next 属性,使其指向待删 除节点的后继;设置该节点后继的 previous 属性,使其指向待删除节点的前驱。

《数据结构与算法-JavaScript描述》--数据结构_第7张图片

JavaScript实现双向链表

function Node(element) {
    this.element = element;
    this.next = null;
    this.previous = null;
 }
 function LList() {
    this.head = new Node("head"); 
    this.find = function(item){
        var currNode = this.head;
        while (currNode.element != item) {
            currNode = currNode.next;
        }
        return currNode;
    };
    //双向链表的 insert() 方法和单向链表的类似,但是需要设置新节点的 previous 属性,使 其指向该节点的前驱。
    this.insert = function(newElement,item){
        var newNode = new Node(newElement); 
        var current = this.find(item); 
        newNode.next = current.next; 
        newNode.previous = current; 
        current.next = newNode;
    }; 
    this.display = function(){
        var currNode = this.head;
        while (!(currNode.next == null)) {
            console.log(currNode.next.element);
            currNode = currNode.next;
        }
    };
    this.dispReverse = function(){
        var currNode = this.head;
        currNode = this.findLast();
        while (!(currNode.previous == null)) {
            console.log(currNode.element);
            currNode = currNode.previous;
        }
    }; 
    this.findLast = function(){
        var currNode = this.head;
        while (!(currNode.next == null)) {
            currNode = currNode.next;
        }
        return currNode;
    }
    this.remove = function(item){
        var currNode = this.find(item); 
        if (!(currNode.next == null)) {
            currNode.previous.next = currNode.next;
            currNode.next.previous = currNode.previous;
            currNode.next = null;
            currNode.previous = null;
        }
    };
 }

var cities = new LList();
cities.insert("Conway", "head");
cities.insert("Russellville", "Conway");
cities.insert("Carlisle", "Russellville");
cities.insert("Alma", "Carlisle");
cities.display();//Conway  Russellville  Carlisle Alma

循环链表

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

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

只需要修改一处,就将单向链表变成了循环链表。但是其他一些方法需要修改才能工作正 常。比如,display() 就需要修改,原来的方式在循环链表里会陷入死循环。while 循环的 循环条件需要修改,需要检查头节点,当循环到头节点时退出循环。

循环链表的 display() 方法如下所示:

     function display() {
        var currNode = this.head;
        while (!(currNode.next == null) &&
               !(currNode.next.element == "head")) {
           print(currNode.next.element);
           currNode = currNode.next;
} }

知道了怎么修改 display() 方法,就应该会修改其他方法了。

6、字典

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

JavaScript实现字典

    function Dictionary() {
        this.datastore = new Array();
        // add():该方法接受两个参数:键和值。键是值在字典中的索引。
        this.add = function(key, value){
            this.datastore[key] = value;
            this.datastore.length++;
            console.log(this.datastore)
        };
        // find():该方法以键作为参数,返回和其关联的值。
        this.find = function(key){
            return this.datastore[key];
        };
        // remove():删除一组键值对
        this.remove = function(key){
            delete this.datastore[key];
            this.datastore.length--;
        };
        // 显示字典中所有的键 - 值对
        this.showAll = function(){
            for(var key in this.datastore) {
                console.log(key,this.datastore[key]);
            }
        };
        // 字典中的元素个数
        this.count = function(){
            return this.datastore.length;
        };
        // 清空字典
        this.clear = function(){
            for(var key in this.datastore) {
                delete this.datastore[key];
                this.datastore.length--;
            }
        }
    }
    var pbook = new Dictionary(); 
    pbook.add("Mike","123");
    pbook.add("David", "345");
    pbook.add("Cynthia", "456");
    pbook.showAll();
    pbook.remove("Cynthia");
    console.log(pbook.count());//2
    pbook.clear();
    console.log(pbook.count());//0

7、散列

散列是一种常用的数据存储技术,散列后的数据可以快速地插入或取用。散列使用的数据 结构叫做散列表。在散列表上插入、删除和取用数据都非常快,但是对于查找操作来说却 效率低下,比如查找一组数据中的最大值和最小值。

我们的散列表是基于数组进行设计的。数组的长度是预先设定的,如有需要,可以随时增 加。所有元素根据和该元素对应的键,保存在数组的特定位置,该键和我们前面讲到的字 典中的键是类似的概念。使用散列表存储数据时,通过一个散列函数将键映射为一个数 字,这个数字的范围是 0 到散列表的长度。

理想情况下,散列函数会将每个键值映射为一个唯一的数组索引。然而,键的数量是无限 的,数组的长度是有限的(理论上,在 JavaScript 中是这样),一个更现实的目标是让散列 函数尽量将键均匀地映射到数组中。

即使使用一个高效的散列函数,仍然存在将两个键映射成同一个值的可能,这种现象称为 碰撞(collision),当碰撞发生时,我们需要有方案去解决。

要确定的最后一个问题是:散列表中的数组究竟应该有多大?这是编写散列函数时必须要 考虑的。对数组大小常见的限制是:数组长度应该是一个质数。

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

为了避免碰撞,首先要确保散列表中用来存储数据的数组其大小是个质数。这一点很关 键,这和计算散列值时使用的取余运算有关。数组的长度应该在 100 以上,这是为了让数 据在散列表中分布得更加均匀。通过试验我们发现,比 100 大且不会让例 8-1 中的数据产 生碰撞的第一个质数是 137。使用其他更接近 100 的质数,在该数据集上依然会产生碰撞。

为了避免碰撞,在给散列表一个合适的大小后,接下来要有一个计算散列值的更好方法。 霍纳算法很好地解决了这个问题。本书不会过多深入该算法的数学细节,在此算法中,新 的散列函数仍然先计算字符串中各字符的 ASCII 码值,不过求和时每次要乘以一个质数。 大多数算法书建议使用一个较小的质数,比如 31,但是对于我们的数据集,31 不起作用, 我们使用 37,这样刚好不会产生碰撞。

综上,用JavaScript实现字符串的散列

function HashTable(){
    // 创建一个长度为137的数组
    this.table = new Array(137);
    // 为了避免碰撞,在给散列表一个合适的大小后,接下来要有一个计算散列值的更好方法。 霍纳算法很好地解决了这个问题。本书不会过多深入该算法的数学细节,在此算法中,新 的散列函数仍然先计算字符串中各字符的 ASCII 码值,不过求和时每次要乘以一个质数。 大多数算法书建议使用一个较小的质数,比如 31,但是对于我们的数据集,31 不起作用, 我们使用 37,这样刚好不会产生碰撞。
    this.betterHash = function(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);
    };
    // 显示散列中的所有数据
    this.showDistro = function(){
        var n = 0;
        for (var i = 0; i < this.table.length; ++i) { 
            if (this.table[i] != undefined) {
                console.log(i + ": " + this.table[i]);
            }
        }
    };
    // 向散列中添加数据
    this.put = function(data){
        var pos = this.betterHash(data);
        this.table[pos] = data;
    };

}

    var someNames = ["David", "Jennifer", "Donnie", "Raymond",
    "Cynthia", "Mike", "Clayton", "Danny", "Jonathan"];
    var hTable = new HashTable();
    for (var i = 0; i < someNames.length; ++i) {
        hTable.put(someNames[i]);
    }
    hTable.showDistro();

结果:

《数据结构与算法-JavaScript描述》--数据结构_第8张图片

碰撞处理:
当散列函数对于多个输入产生同样的输出时,就产生了碰撞。

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

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

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

8、集合

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

下面是一些使用集合时必须了解的定义。
• 不包含任何成员的集合称为空集,全集则是包含一切可能成员的集合。
• 如果两个集合的成员完全相同,则称两个集合相等。
• 如果一个集合中所有的成员都属于另外一个集合,则前一集合称为后一集合的子集。

对集合的基本操作有下面几种。
• 并集 将两个集合中的成员进行合并,得到一个新集合。
• 交集 两个集合中共同存在的成员组成一个新的集合。
• 补集 属于一个集合而不属于另一个集合的成员组成的集合。

JavaScript实现集合

    function Set() {
        // 用数组去保存集合
        this.dataStore = [];
        // 向集合中添加数据
        this.add = function(data){
            if (this.dataStore.indexOf(data) < 0) {
                this.dataStore.push(data);
                return true;
            } else {
                return false;
            }
        };
        // 从集合中删除数据
        this.remove = function(){
            var pos = this.dataStore.indexOf(data);
            if (pos > -1) {
                this.dataStore.splice(pos,1);
                return true;
            } else {
                return false;
            }
        };
        // 集合长度
        this.size = function(){
            return this.dataStore.length;
        };
        // 并集
        this.union = function(set){
            var tempSet = new Set();
            for (var i = 0; i < this.dataStore.length; ++i) {
                tempSet.add(this.dataStore[i]);
            }
            for (var i = 0; i < set.dataStore.length; ++i) {
                if (!tempSet.contains(set.dataStore[i])) {
                    tempSet.dataStore.push(set.dataStore[i]);
                }
            }
            return tempSet
        };
        // 交集
        this.intersect = function(set){
            var tempSet = new Set();
            for (var i = 0; i < this.dataStore.length; ++i) {
                if (set.contains(this.dataStore[i])) {
                    tempSet.add(this.dataStore[i]);
                } 
            }
            return tempSet;
        };
        // 子集
        this.subset = function(set){
            if (this.size() > set.size()) {
                return false;
            } else {
                for (var i = 0 ; i < this.dataStore.length; i++){
                    if (!set.contains(this.dataStore[i])) {
                        return false;
                    } 
                }
            }
            return true
        };
        // 补集
        this.difference = function(set){
            var tempSet = new Set();
            for (var i = 0; i < this.dataStore.length; ++i) {
                if (!set.contains(this.dataStore[i])) {
                    tempSet.add(this.dataStore[i]);
                } 
            }
            return tempSet;
        };
        // 显示集合中的所有数据
        this.show = function(){
            return this.dataStore;
        };
        // 判断集合中是否包含某个元素
        this.contains = function (data){
            if (this.dataStore.indexOf(data) > -1) {
                return true;
            }
            else {
                return false;
            }
        }
    }
    var cis = new Set(); 
    cis.add("Raymond");
    cis.add("Cynthia"); 
    var dmp = new Set(); 
    dmp.add("Raymond"); 
    dmp.add("Cynthia"); 
    dmp.add("Bryan");
    console.log(cis.subset(dmp)); //true

9、二叉树和二叉查找树

树是计算机科学中经常用到的一种数据结构。树是一种非线性的数据结构,以分层的方式 存储数据。树被用来存储具有层级关系的数据,比如文件系统中的文件;树还被用来存储 有序列表。本章将研究一种特殊的树:二叉树。选择树而不是那些基本的数据结构,是因 为在二叉树上进行查找非常快(而在链表上查找则不是这样),为二叉树添加或删除元素 也非常快(而对数组执行添加或删除操作则不是这样)


《数据结构与算法-JavaScript描述》--数据结构_第9张图片

一棵树最上面的节点称为 根节点,如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子 节点。一个节点可以有 0 个、1 个或多个子节点。没有任何子节点的节点称为叶子节点。

二叉树是一种特殊的树,它的子节点个数不超过两个。

从一个节点到另一个节点的这一组边称为路径,在图中用虚线表示。以某种特定顺序 访问树中所有的节点称为树的遍历。

二叉查找树是一种 特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中。这一特性使得 查找的效率很高,对于数值型和非数值型的数据,比如单词和字符串,都是如此。

JavaScript实现二叉查找树(注意看注释)

// Node 对象既保存数据,也保存和其他节点的链接(left 和 right),show() 方法用来显示 保存在节点中的数据。
function Node(data,left,right){
    this.data = data;
    this.count = 1;
    this.left = left;
    this.right = right;
    this.show = function(){
        return console.log(this.data);
    }
}
// 现在可以创建一个类,用来表示二叉查找树(BST)。我们让类只包含一个数据成员:一个 表示二叉查找树根节点的 Node 对象。该类的构造函数将根节点初始化为 null,以此创建 一个空节点。
function BST(){
    this.root = null;
    // 用来向树中加入新节点
    // 检查 BST 是否有根节点,如果没有,那么这是棵新树,该节点就是根节点,这个方法 到此也就完成了;否则,进入下一步。如果待插入节点不是根节点,那么就需要准备遍历 BST,找到插入的适当位置。该过程类 似于遍历链表。用一个变量存储当前节点,一层层地遍历 BST。
    // (1) 设根节点为当前节点。
    // (2) 如果待插入节点保存的数据小于当前节点,则设新的当前节点为原节点的左节点;反
    // 之,执行第 4 步。
    // (3) 如果当前节点的左节点为 null,就将新的节点插入这个位置,退出循环;反之,继续
    // 执行下一次循环。
    // (4) 设新的当前节点为原节点的右节点。
    // (5) 如果当前节点的右节点为 null,就将新的节点插入这个位置,退出循环;反之,继续
    // 执行下一次循环。
    this.insert = function(data){
        var n = new Node(data, null, null);
        if (this.root == null) {
            this.root = n;
        } else {
            var current = this.root;
            var parent;
            while (true) {
                parent = current;
                if (data < current.data) {
                    current = current.left;
                    if (current == null) {
                        parent.left = n;
                        break; 
                    }
                } else {
                    current = current.right;
                    if (current == null) {
                        parent.right = n;
                        break; 
                    }
                } 
            }
        }
    };
    // 中序遍历的访问路径
    // 中序遍历按照节点上的键值,以升序访问 BST 上的所有节点。先序遍历先访问根节点,然后以同样方式访问左子树和右子树。后序 遍历先访问叶子节点,从左子树到右子树,再到根节点。
    this.inOrder = function(node){
        if(!(node == null)){
            this.inOrder(node.left);
            node.show();
            this.inOrder(node.right);
        }
    };
    // 先序遍历
    // 注意 inOrder() 和 preOrder() 方法的唯一区别,就是 if 语句中代码的顺序。在 inOrder() 方法中,show() 函数像三明治一样夹在两个递归调用之间;在 preOrder() 方法中,show() 函数放在两个递归调用之前。
    this.preOrder = function(node){
        if(!(node == null)){
            node.show();
            this.inOrder(node.left);
            this.inOrder(node.right);
        }
    }
    // 后序遍历
    this.postOrder = function(node){
        if(!(node == null)){
            this.inOrder(node.left);
            this.inOrder(node.right);
            node.show();
        }
    }
    // 查找最小值
    // 查找 BST 上的最小值和最大值非常简单。因为较小的值总是在左子节点上,在 BST 上查找最小值,只需要遍历左子树,直到找到最后一个节点。
    this.getMin = function(){
        var current = this.root;
        while (!(current.left == null)) {
            current = current.left;
        }
        return current.data;
    }
    // 查找最大值
    // 在 BST 上查找最大值,只需要遍历右子树,直到找到最后一个节点,该节点上保存的值即 为最大值。
    this.getMax = function(){
        var current = this.root;
        while (!(current.right == null)) {
            current = current.right;
        }
        return current.data;
    }
    // 查找给定值
    // 在 BST 上查找给定值,需要比较该值和当前节点上的值的大小。通过比较,就能确定如果给定值不在当前节点时,该向左遍历还是向右遍历。
    this.find = function(data){
        var current = this.root;
        while (current != null) {
            if (current.data == data) {
                return current;
            } else if (data < current.data) {
                current = current.left;
            } else {
                current = current.right;
            }
        }
        return null;
    }
    // 从二叉查找树上删除节点
    // 从 BST 中删除节点的第一步是判断当前节点是否包含待删除的数据,如果包含,则删除该 节点;如果不包含,则比较当前节点上的数据和待删除的数据。如果待删除数据小于当前 节点上的数据,则移至当前节点的左子节点继续比较;如果删除数据大于当前节点上的数 据,则移至当前节点的右子节点继续比较。
    // 如果待删除节点是叶子节点(没有子节点的节点),那么只需要将从父节点指向它的链接 指向 null。
    // 如果待删除节点只包含一个子节点,那么原本指向它的节点就得做些调整,使其指向它的子节点。
    // 最后,如果待删除节点包含两个子节点,正确的做法有两种:要么查找待删除节点左子树 上的最大值,要么查找其右子树上的最小值。
    this.remove = function(data){
        root = this.removeNode(this.root,data)
    }
    this.removeNode = function(node,data){
        if (node == null) {
            return null;
        }
        if (data == node.data) {
        // 没有子节点的节点
            if (node.left == null && node.right == null) {
                return null;
            }
            // 没有左子节点的节点
            if (node.left == null) {
                return node.right;
            }
            // 没有右子节点的节点
            if (node.right == null) {
                return node.left;
            }
            // 有两个子节点的节点
            var tempNode = getSmallest(node.right);
            node.data = tempNode.data;
            node.right = removeNode(node.right, tempNode.data); return node;
        } else if (data < node.data) {
            node.left = removeNode(node.left, data);
            return node;
        } else {
            node.right = removeNode(node.right, data);
            return node;
        }
    }
    // 计数
    // BST 的一个用途是记录一组数据集中数据出现的次数。比如,可以使用 BST 记录考试成 绩的分布。给定一组考试成绩,可以写一段程序将它们加入一个 BST,如果某成绩尚未在 BST 中出现,就将其加入 BST;如果已经出现,就将出现的次数加 1。
    // 为了解决该问题,我们来修改 Node 对象,为其增加一个记录成绩出现频次的成员,同时我 们还需要一个方法,当在 BST 中发现某成绩时,需要将出现的次数加 1,并且更新该节点。
    this.update = function update(data){
        var grade = this.find(data); 
        grade.count++;
        return grade;
    }
}
    var nums = new BST();
    nums.insert(23);
    nums.insert(45);
    nums.insert(16);
    nums.insert(37);
    nums.insert(3);
    nums.insert(99);
    nums.insert(99);
    nums.insert(22);
    // console.log("Inorder traversal: ");
    // nums.inOrder(nums.root);
    // nums.preOrder(nums.root)
    nums.postOrder(nums.root);
    console.log(nums.getMin());
    console.log(nums.getMax());
    console.log(nums.find(23));
    console.log(nums.update(99))
    console.log(nums.removeNode(99))
    console.log(nums.update(99))

10、图和图算法

后续。

整理不易,且看且珍惜。

你可能感兴趣的:(《数据结构与算法-JavaScript描述》--数据结构)