分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)

Trie树(字典树、前缀树)的基础知识

 

字典树是一种有序的,用于统计、排序和存储字符串的数据结构,他与二叉查找树不同,关键字不是直接保存在节点中,而是由节点在树中的位置决定,每个节点代表了一个字符,从第一层孩子节点到中间的某个标记的节点代表了存储的字符串

一个节点的所有子孙都有相同的前缀,而根节点对应空字符串。

一般情况下,不是所有的节点都有对应的字符串,只有叶子节点和部分内部节点进行标记,才存储了字符串。

字典树的最大优点就是利用字符串的公共前缀来减少存储空间与查询时间,从而最大限度的减少无味的字符串比较。查找和插入字符串都可达到O(1)算法复杂度。

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

字典树的节点表示

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;  //代表某个字符串是否以该节点作为结尾
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};

 

 

字典树构造的例子

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};

int main(int argc, char *argv[]) {
	TrieNode root;
	TrieNode n1;
	TrieNode n2;
	TrieNode n3;
	root.child['a' - 'a'] = &n1;
	root.child['b' - 'a'] = &n2;
	root.child['e' - 'a'] = &n3;
	n2.is_end = true;

	TrieNode n4;
	TrieNode n5;
	TrieNode n6;
	n1.child['b' - 'a'] = &n4;
	n2.child['c' - 'a'] = &n5;
	n3.child['f' - 'a'] = &n6;


	TrieNode n7;
	TrieNode n8;
	TrieNode n9;
	TrieNode n10;
	n4.child['c' - 'a'] = &n7;
	n4.child['d' - 'a'] = &n8;
	n5.child['d' - 'a'] = &n9;
	n6.child['g' - 'a'] = &n10;
	n7.is_end = true;
	n8.is_end = true;
	n9.is_end = true;
	n10.is_end = true;

	TrieNode n11;
	n7.child['d' - 'a'] = &n11;
	n11.is_end = true;

	return 0;
}

 

 

字典树的前序遍历

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};

//先序遍历
void preorder_trie(TrieNode *node, int layer) {
	for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
		if (node->child[i]) {
			for (int j = 0; j < layer; j++) {
				printf("---");
			}
			printf("%c", i + 'a');
			if (node->child[i]->is_end) {
				printf("(end)");
			}
			printf("\n");
			preorder_trie(node->child[i], layer + 1);
		}
	}
}

int main(int argc, char *argv[]) {
	TrieNode root;
	TrieNode n1;
	TrieNode n2;
	TrieNode n3;
	root.child['a' - 'a'] = &n1;
	root.child['b' - 'a'] = &n2;
	root.child['e' - 'a'] = &n3;
	n2.is_end = true;

	TrieNode n4;
	TrieNode n5;
	TrieNode n6;
	n1.child['b' - 'a'] = &n4;
	n2.child['c' - 'a'] = &n5;
	n3.child['f' - 'a'] = &n6;


	TrieNode n7;
	TrieNode n8;
	TrieNode n9;
	TrieNode n10;
	n4.child['c' - 'a'] = &n7;
	n4.child['d' - 'a'] = &n8;
	n5.child['d' - 'a'] = &n9;
	n6.child['g' - 'a'] = &n10;
	n7.is_end = true;
	n8.is_end = true;
	n9.is_end = true;
	n10.is_end = true;

	TrieNode n11;
	n7.child['d' - 'a'] = &n11;
	n11.is_end = true;

	preorder_trie(&root, 0);
	return 0;
}

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

获取字典树中全部单词

 

深度遍历字典树,对于正在搜索的节点node,遍历该节点的26个孩子指针 child[i](‘a-‘z’) ,如果指针不为空,则将该child[i]对应的字符 ( i +’a’)压入中。

如果该孩子指针标记的  is_end 为真,则从栈底到栈顶对栈进行遍历,生成字符串,将他保存到结果数组中。

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》     《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

实现代码:

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};

void get_all_word_from_trie(TrieNode *node, 
		string &word, vector<string>&word_list) {
	for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
		if (node->child[i]) {
			word.push_back(i + 'a');
			if (node->child[i]->is_end) {
				word_list.push_back(word);
			}
			get_all_word_from_trie(node->child[i], word, word_list);
			word.erase(word.length() - 1, 1);
		}
	}
}

 

 

字典树的整体功能

class TrieTree {
public:
	TrieTree(){}
	~TrieTree() {
		for (int i = 0; i < _node_vec.size(); i++) {
			delete _node_vec[i];
		}
	}

	//将word插入至trie
	void insert(const char *word) {}
	//搜索trie中是否存在Word
	bool search(const char *word) {}
	//确认trie中是否有前缀为prefox的单词
	bool startsWith(const char *prefix) {}
	TrieNode *root() {
		return &_root;
	}
private:
	TrieNode *new_node() {
		TrieNode *node = new TrieNode();
		_node_vec.push_back(node);
		return node;
	}
	vector<TrieNode*>_node_vec;
	TrieNode _root;
};

 

字典树的插入操作

对于一个字典树,已保存 abc、abcd,待插入abd

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

插入操作的步骤:

  1. 使用指针ptr指向root,逐个遍历待插入的字符串中的各个字符:
  2. 计算下标 pos=正在遍历的字符-‘a’
  3. 如果 ptr 指向的节点的第 pos 个孩子为假的,则创建该节点的第 pos 个孩子,并且使 prt 指向该节点的第 pos 个孩子
  4. 标记 ptr 指向的节点的  is_end 为 true。
void TrieTree::insert(const char *word) {
	TrieNode *ptr = &_root;
	while (*word) {
		int pos = *word - 'a';
		if (!ptr->child[pos]) {
			ptr->child[pos] = new_node();
		}
		ptr = ptr->child[pos];
		word++;
	}
	ptr->is_end = true;
}

 

 

字典树的搜索操作

操作步骤如下:

1、使指针 ptr 指向root

2、逐个遍历待搜索的字符串中的各个字符:

  • 计算下标 pos=正在遍历的字符-‘a’
  • 如果ptr指向的节点的第 pos 个孩子为假,返回false
  • ptr指向该节点的第ps个孩子

3、返回ptr指向的节点  is_end

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

bool TrieTree::search(const char *word) {
	TrieNode *ptr = &_root;
	while (*word) {
		int pos = *word - 'a';
		if (!ptr->child[pos]) {
			return false;
		}
		ptr = ptr->child[pos];
		word++;
	}
	return ptr->is_end;
}

 

 

字典树的前缀查询

确认trie中是否有前缀为prefox的单词

bool TrieTree::startsWith(const char *prefix) {
	TrieNode *ptr = &_root;
	while (*prefix) {
		int pos = *prefix - 'a';
		if (!ptr->child[pos]) {
			return false;
		}
		ptr = ptr->child[pos];
		prefix++;
	}
	return true;
}

 

 

字典树的整体实现代码

class TrieTree {
public:
	TrieTree(){}
	~TrieTree() {
		for (int i = 0; i < _node_vec.size(); i++) {
			delete _node_vec[i];
		}
	}

	//将word插入至trie
	void insert(const char *word) {}
	//搜索trie中是否存在Word
	bool search(const char *word) {}
	//确认trie中是否有前缀为prefox的单词
	bool startsWith(const char *prefix) {}
	TrieNode *root() {
		return &_root;
	}
private:
	TrieNode *new_node() {
		TrieNode *node = new TrieNode();
		_node_vec.push_back(node);
		return node;
	}
	vector<TrieNode*>_node_vec;
	TrieNode _root;
};


void TrieTree::insert(const char *word) {
	TrieNode *ptr = &_root;
	while (*word) {
		int pos = *word - 'a';
		if (!ptr->child[pos]) {
			ptr->child[pos] = new_node();
		}
		ptr = ptr->child[pos];
		word++;
	}
	ptr->is_end = true;
}

bool TrieTree::search(const char *word) {
	TrieNode *ptr = &_root;
	while (*word) {
		int pos = *word - 'a';
		if (!ptr->child[pos]) {
			return false;
		}
		ptr = ptr->child[pos];
		word++;
	}
	return ptr->is_end;
}

bool TrieTree::startsWith(const char *prefix) {
	TrieNode *ptr = &_root;
	while (*prefix) {
		int pos = *prefix - 'a';
		if (!ptr->child[pos]) {
			return false;
		}
		ptr = ptr->child[pos];
		prefix++;
	}
	return true;
}

 

 

 

leetcode 208 实现 Trie (前缀树)

实现一个 Trie (前缀树),包含 insertsearch, 和 startsWith 这三个操作。

示例:

Trie trie = new Trie();

trie.insert("apple");
trie.search("apple");   // 返回 true
trie.search("app");     // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");   
trie.search("app");     // 返回 true

说明:

  • 你可以假设所有的输入都是由小写字母 a-z 构成的。
  • 保证所有输入均为非空字符串。

思路:

实现思路在上方

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};

class Trie {
public:
    /** Initialize your data structure here. */
    Trie() { }
    
    ~Trie() {
		for (int i = 0; i < _node_vec.size(); i++) {
			delete _node_vec[i];
		}
	}
    /** Inserts a word into the trie. */
    void insert(string word) {
        TrieNode *ptr = &_root;
        int index=0;
        while (word[index]) {
            int pos = word[index] - 'a';
            if (!ptr->child[pos]) {
                ptr->child[pos] = new_node();
            }
            ptr = ptr->child[pos];
            index++;
        }
        ptr->is_end = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        TrieNode *ptr = &_root;
        int index=0;
        while (word[index]) {
            int pos = word[index] - 'a';
            if (!ptr->child[pos]) {
                return false;
            }
            ptr = ptr->child[pos];
            index++;
        }
        return ptr->is_end;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        TrieNode *ptr = &_root;
        int index=0;
        while (prefix[index]) {
            int pos = prefix[index] - 'a';
            if (!ptr->child[pos]) {
                return false;
            }
            ptr = ptr->child[pos];
            index++;
        }
        return true;
    }
    
private:
    TrieNode *new_node() {
        TrieNode *node = new TrieNode();
        _node_vec.push_back(node);
        return node;
    }
    vector<TrieNode*>_node_vec;
    TrieNode _root;
};

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

 

leetcode 211 添加与搜索单词 – 数据结构设计

设计一个支持以下两种操作的数据结构:

void addWord(word)
bool search(word)

search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z 。 .可以表示任何一个字母。

示例:

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

说明:

你可以假设所有单词都是由小写字母 a-z 组成的。

思路:

嵌套字典树,add操作就是字典树的insert操作。对于search的 “ . ” 的情况:

node为正在搜索的节点,Word指向正在搜索的单词起始,查找成功返回真,否则返回假。

bool search_trie(TrieNode *node, const char *word);

1、当遍历到单词的结尾(即“\0”)

  • 如果node指向的节点的标记是单词的结尾(is_end为真),则返回真
  • 否则返回假

2、如果Word指向 “ .

  • 遍历node的全部孩子指针,如果孩子指针为真,则继续深搜该孩子子树,单词指针向前移动一个位置。
  • 如果递归深搜的结果为真,则返回真。

3、如果Word指向(‘a’-‘z’)

  • 计算孩子位置  pos=当前字符-‘a’
  • 如果pos指向的孩子指针为真,继续递归深搜该孩子子树,单词指针向前移动一个位置,如果递归深搜结果为真,则返回真。

4、最终返回假。

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》          《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

bool search_trie(TrieNode *node, const char *word) {
       if(*word=='\0'){
           if(node->is_end) return true;
           return false;
       }
        if(*word=='.'){
            for(int i=0; i<TRIE_MAX_CHAR_NUM; i++){
                if(node->child[i]&&
                  search_trie(node->child[i], word+1))
                    return true;
            }
        }
        else{
            int pos=*word-'a';
            if(node->child[pos]&&
              search_trie(node->child[pos], word+1))
                return true;
        }
        return false;
    }    

 

完整实现代码:

#define TRIE_MAX_CHAR_NUM 26

struct TrieNode {
	TrieNode *child[TRIE_MAX_CHAR_NUM];
	bool is_end;
	TrieNode() :is_end(false) {
		for (int i = 0; i < TRIE_MAX_CHAR_NUM; i++) {
			child[i] = 0;
		}
	}
};
    
class Trie {
public:
    /** Initialize your data structure here. */
    Trie() { }
    
    ~Trie() {
        for (int i = 0; i < _node_vec.size(); i++) {
            delete _node_vec[i];
        }
	}
    /** Inserts a word into the trie. */
    void insert(string word) {
        TrieNode *ptr = &_root;
        int index=0;
        while (word[index]) {
            int pos = word[index] - 'a';
            if (!ptr->child[pos]) {
                ptr->child[pos] = new_node();
            }
            ptr = ptr->child[pos];
            index++;
        }
        ptr->is_end = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search_trie(TrieNode *node, const char *word) {
       if(*word=='\0'){
           if(node->is_end) return true;
           return false;
       }
        if(*word=='.'){
            for(int i=0; i<TRIE_MAX_CHAR_NUM; i++){
                if(node->child[i]&&
                  search_trie(node->child[i], word+1))
                    return true;
            }
        }
        else{
            int pos=*word-'a';
            if(node->child[pos]&&
              search_trie(node->child[pos], word+1))
                return true;
        }
        return false;
    }    
    
    TrieNode* root(){
		return &_root;
	}

    
private:
    TrieNode *new_node() {
        TrieNode *node = new TrieNode();
        _node_vec.push_back(node);
        return node;
    }
    vector<TrieNode*>_node_vec;
    TrieNode _root;
};



class WordDictionary {
public:
    /** Initialize your data structure here. */
    WordDictionary() {}
    
    /** Adds a word into the data structure. */
    void addWord(string word) {
        _trie_tree.insert(word);
    }
    
    /** Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. */
    bool search(string word) {
        return _trie_tree.search_trie(_trie_tree.root(), word.c_str());
    }
private:
    Trie _trie_tree;
};

/**
 * Your WordDictionary object will be instantiated and called as such:
 * WordDictionary obj = new WordDictionary();
 * obj.addWord(word);
 * bool param_2 = obj.search(word);
 */

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

 

并查集

 

基本概念

并查集(Union Find),又称为不相交集合(Disjiont Set),他应用于N个元素的集合求并查询问题。

应用时,我们通常是在开始时让每个元素构成一个单元素的集合,然后按照一定顺序将同属于同一组的元素所在的集合合并,期间要反复查找一个元素在哪个集合中。

虽然问题并不复杂,但是面对极大的数据量时,普通的数据结构往往无法解决,并查集就是解决该种问题的最优秀的算法。

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

功能分析

初始时:

  • 原始元素:0 1 2 3 4 5 6 7 
  • 原始集合:0 1 2 3 4 5 6 7

执行 Union(0, 5)之后,0和5属于同一个集合

  • 元素:0 1 2 3 4 5 6 7
  • 集合:0 1 2 3 4 0 6 7

此时执行:

  • Find(0)==0
  • Find(5)==0
  • Find(0)==Find(5),在同一个集合
  • Find(2)==2
  • Find(5)==0
  • Find(2) !=Find(5),不在同一个集合

 

数组实现并查集

class DisjointSet {
public:
	DisjointSet(int n) {
		for (int i = 0; i < n; i++) {
			_id.push_back(i);
		}
	}
	//查询元素 p 属于哪个集合时返回 id[i]
	int find(int p) {
		return _id[p];
	}
	//合并时,若两个元素属于同一个集合,则直接返回
	void union_(int p, int q) {
		int pid = find(p);
		int qid = find(q);
		if (pid == qid) return;

		//将所有属于pd集合的全部改为qid对应的集合
		for (int i = 0; i < _id.size(); i++) {
			if (_id[i] == pid)
				_id[i] = qid;
		}
	}

private:
	//设置表示集合数组id[i],初始时每个元素构成一个单元素的集合
	//编号为 i 的元素属于集合 i
	vector<int>_id;
};

 

森林实现并查集

使用森林存储集合之间的关系。属于同一集合不同元素,都有相同的根节点,用根节点表示集合

当进行查找某元素属于哪个集合时,即遍历该元素到根节点,返回根节点所代表的集合;在遍历过程中使用路径压缩的优化算法,使整体树的形状更加扁平,从而优化查询的时间复杂度

当进行合并时,即将两棵子树合并为一棵树,将一颗子树的根节点指向另一颗子树的根节点;在合并时,可按照子树的大小,将规模较小的子树合并到规模较大的子树上,从而使树规模更加平衡,从而优化未来的查询时间复杂度

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

并查集的查找算法分析:

1、在查找时,普通的查找即通过id数组遍历至根节点:

  • 当 p 与当前集合 id[p] 不同时,进行循环,执行 p=id[p]。直到 p 与 id[p] 相同时跳出循环。最终返回 p的值。

2、在查找时增加路径压缩的优化算法:

  • 当 p 与当前集合 id[p] 不同时,进行循环,循环过程中,将 p 的父节点 id[p] 更新为 id[p] 的父节点 id[ id[ p ] ], 之后执行 p=id[p]。直到 p 与 id[p] 相同时跳出循环。最终返回 p的值。

 

代码实现:

int find(int p) {
    while (p != _id[p]) {
        _id[p] = _id[_id[p]];
        p = _id[p];
    }
    return p;
}

 

并查集的合并算法分析:

当进行集合的合并时,即将两棵子树合并为一棵树,将一棵树的根节点指向另一棵树的根节点。合并时将尺寸小的树合并到规模更大的树上,使得树更加平衡。

算法步骤:

1、合并 p 所在的集合与 q 所在的集合:

  • 查找p所在集合的根, i = find(p);
  • 查找q所在集合的根, j = find(q);

2、如果 i 与 j 相同,则直接返回。

3、如果 i 所在子树规模小于 j 所在子树规模

  • 将 i 的根指向 j
  • 更新 j 的规模为 j + i 的规模

4、否则:

  • 将 j 的根指向 i
  • 更新 i 的规模为 j + i 的规模

5、子树个数减 1

//合并时,若两个元素属于同一个集合,则直接返回
void union_(int p, int q) {
    int i = find(p);
    int j = find(q);
    if (i == j) return;

    if (_size[i] < _size[j]) {
        _id[i] = j;
        _size[j] += _size[i];
    }
    else {
        _id[j] = i;
        _size[i] += _size[j];
    }
    _count--;
}

 

并查集的整体实现代码:

class DisjointSet {
public:
	DisjointSet(int n) {
		for (int i = 0; i < n; i++) {
			_id.push_back(i);
			_size.push_back(1);
		}
		_count = n;
	}
	//查询元素 p 属于哪个集合时返回 id[i]
	int find(int p) {
		while (p != _id[p]) {
			_id[p] = _id[_id[p]];
			p = _id[p];
		}
		return p;
	}
	//合并时,若两个元素属于同一个集合,则直接返回
	void union_(int p, int q) {
		int i = find(p);
		int j = find(q);
		if (i == j) return;

		if (_size[i] < _size[j]) {
			_id[i] = j;
			_size[j] += _size[i];
		}
		else {
			_id[j] = i;
			_size[i] += _size[j];
		}
		_count--;
	}

        int count(){
            return _count;
        }

private:
	//设置表示集合数组id[i],初始时每个元素构成一个单元素的集合
	//编号为 i 的元素属于集合 i
	vector<int>_id;
	vector<int>_size;
	int _count;
};

 

 

 

leetcode 547 朋友圈

班上有 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

示例 1:

输入: 
[
 [1,1,0], 
 [1,1,0], 
 [0,0,1]
]
输出: 2 
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。第2个学生自己在一个朋友圈。所以返回2。
(第一个数组代表第0个学生,第一个数组中的每个元素代表第0个学生和谁是朋友,是朋友的就标1)

示例 2:

输入: 
[ 
 [1,1,0], 
 [1,1,1], 
 [0,1,1]
]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。

注意:

  1. N 在[1,200]的范围内。
  2. 对于所有学生,有M[i][i] = 1。
  3. 如果有M[i][j] = 1,则有M[j][i] = 1。

思路:

方法一——图的深度优先搜索

  • 图中有多少个区域的相连的,可以使用图的深搜或广搜来解决。
class Solution {
public:
    void DFS_graph(int u, vector<vector<int>>& graph, 
                   vector<int>& visit){
        //访问过的节点标记为 1
        visit[u]=1;
        for(int i=0; i<graph[u].size(); i++){
            //节点未被访问过且是连通的
            if(visit[i]==0 && graph[u][i]==1)
                DFS_graph(i, graph, visit);
        }
    }
    
    int findCircleNum(vector<vector<int>>& M) {
        //记录哪个节点被搜过
        vector<int> visit(M.size(), 0);
        int count=0;
        for(int i=0; i<M.size(); i++){
            if(visit[i]==0){
                DFS_graph(i, M, visit);
                count++;
            }
        }
        return count;
    }
};

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

方法二——并查集

class DisjointSet {
public:
	DisjointSet(int n) {
		for (int i = 0; i < n; i++) {
			_id.push_back(i);
			_size.push_back(1);
		}
		_count = n;
	}
	//查询元素 p 属于哪个集合时返回 id[i]
	int find(int p) {
		while (p != _id[p]) {
			_id[p] = _id[_id[p]];
			p = _id[p];
		}
		return p;
	}
	//合并时,若两个元素属于同一个集合,则直接返回
	void union_(int p, int q) {
		int i = find(p);
		int j = find(q);
		if (i == j) return;

		if (_size[i] < _size[j]) {
			_id[i] = j;
			_size[j] += _size[i];
		}
		else {
			_id[j] = i;
			_size[i] += _size[j];
		}
		_count--;
	}
    
    int count(){
        return _count;
    }

private:
	//设置表示集合数组id[i],初始时每个元素构成一个单元素的集合
	//编号为 i 的元素属于集合 i
	vector<int>_id;
	vector<int>_size;
	int _count;
};

class Solution {
public:
    int findCircleNum(vector<vector<int>>& M) {
        DisjointSet disjoint_set(M.size());
        for(int i=0; i<M.size(); i++){
            for(int j=i+1; j<M.size(); j++){
                if(M[i][j]){
                    disjoint_set.union_(i,j);
                }
            }
        }
        return disjoint_set.count();
    }
};

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

 

线段树

线段树是一种平衡二叉搜索树(完全二叉树),他将一个线段区间划分成一些单元区间。对于线段树中的每个非叶子节点 [a,b],他的左儿子表示的区间为 [ a, (a+b)/2]右儿子表示的区间为 [(a+b)/2+1, b] , 最后的叶子节点数目为 N, 与数组下标对应。

线段树的一般包括建立、查询、插入、更新等操作,建立规模为 N 的时间复杂度是 O(nlogn),其他操作时间复杂度为O(logn)

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

由于线段树是完全二叉树,线段树可以使用数组保存,如:

将数组[0, 1, 2, 3, 4, 5]保存在存储区间和的线段树中:

  • 根节点下标为 0
  • 设某个节点的下标为 i ,他的左孩子下标为 2*i+1, 右孩子下标为 2*i +2

 

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

线段树的构造

1、传入参数说明:

线段树数组为value,原始数组为nums。当前线段节点在线段树数组value中的下标为 pos,当前线段的左端点left,右端点right。

2、实现步骤:

  • 如果区间的左端点 left 与右端点 right 的值相同,说明该节点是叶节点,将value[pos]赋值为nums[left]或者nums[right]。
  • 计算线段中心: mid=(ledt+right)/2
  • 递归建立左子树线段 [left, mid]
  • 递归建立右子树线段[mid+1, right]
void build_segment_tree(vector<int>&value, 
        vector<int>&nums, int pos, int left, int right) {
        //说明此时是叶节点
	if (left == right) {
		value[pos] = nums[left];
		return;
	}
	int mid = (left + right) / 2;
	build_segment_tree(value, nums, pos * 2 + 1, left, mid);
	build_segment_tree(value, nums, pos * 2 + 2, mid + 1, right);
	value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
}

 

线段树的遍历

void print_segment_tree(vector<int>&value, int pos, int left, int right, int layer) {
	for (int i = 0; i < layer; i++) {
		printf("---");
	}
	printf("[%d %d] [%d]:%d\n", left, right, pos, value[pos]);
	if (left == right) return;
	int mid = (left + right) / 2;
	print_segment_tree(value, pos*2+1, left, mid, layer+1);
	print_segment_tree(value, pos*2+2, mid+1, right, layer + 1);
}

 

线段树求和

1、传入参数说明:

线段树数组为value,当前线段节点在线段树数组value中的下标为 pos,当前线段的左端点left,右端点right,待求和区间的左端点qleft、右端点qright

2、返回值说明:

返回值为该区间的和。

3、实现步骤:

  • 如果区间[left, right]与[qleft, qright]无交集,返回0
  • 如果区间[left, right]被qleft, qright]覆盖,返回该区间的值
  • 否则,返回左子树的访问值+右子树的访问值

4、例子,查询[2, 4]的和

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

查询[0, 5] -> 查询[0, 2] -> 查询[0, 1],返回0 -> 查询[2, 2],返回2 -> 查询[3, 5] -> 查询[3, 4],返回7 -> 查询[5, 5],返回0

最终结果 2+7=9

int sum_range_segment_tree(vector<int>&value, 
		int pos, int left, int right, int qleft, int qright) {

	if (qleft > right || qright < left) return 0;
	if (qleft <= left && qright >= right) return value[pos];
	int mid = (left + right) / 2;

	return sum_range_segment_tree(value, pos * 2 + 1, 
			left, mid, qleft, qright)
		+ sum_range_segment_tree(value, pos * 2 + 2, 
			mid + 1, right, qleft, qright);
}

 

线段树的更新

1、传入参数说明:

线段树数组为value,当前线段节点在线段树数组value中的下标为 pos,当前线段的左端点left,右端点right,待更新的数组索引位置为 index, 更新值为 new_value

2、实现步骤:

  • 如果当前访问的节点为线段树的叶节点,且待更新数组下标为index,则更新该节点的值为 new_value并返回
  • 否则,计算中心点mid
  • 如果index<=mid,递归更新该节点的左子树
  • 否则,递归更新该节点的右子树
  • 当前节点值为  左子树的访问值 + 右子树的访问值

3、例子,更新原数组下标为 2 的值,更新为10

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

搜索 【0,5】

搜索【0,2】

搜索【2,2】

更新【2,2】为10

更新【0,2】为11

更新【0,5】为23

4、实现代码

void update_segment_tree(vector<int>&value, 
		int pos, int left, int right, int index, int new_value) {
	if (left == right && left == index) {
		value[pos] = new_value;
		return;
	}

	int mid = (left + right) / 2;
	if (index <= mid) {
		update_segment_tree(value, pos * 2 + 1, left, mid, index, new_value);
	}
	else {
		update_segment_tree(value, pos * 2 + 2, mid+1, right, index, new_value);
	}

	value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
}

 

线段树的整体实现以及测试代码

#include<iostream>
#include<vector>
using namespace std;

void build_segment_tree(vector<int>&value,
	vector<int>&nums, int pos, int left, int right) {
	if (left == right) {
		value[pos] = nums[left];
		return;
	}
	int mid = (left + right) / 2;
	build_segment_tree(value, nums, pos * 2 + 1, left, mid);
	build_segment_tree(value, nums, pos * 2 + 2, mid + 1, right);
	value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
}

void print_segment_tree(vector<int>&value, int pos, int left, int right, int layer) {
	for (int i = 0; i < layer; i++) {
		printf("---");
	}
	printf("[%d %d] [%d]:%d\n", left, right, pos, value[pos]);
	if (left == right) return;
	int mid = (left + right) / 2;
	print_segment_tree(value, pos * 2 + 1, left, mid, layer + 1);
	print_segment_tree(value, pos * 2 + 2, mid + 1, right, layer + 1);
}

int sum_range_segment_tree(vector<int>&value,
	int pos, int left, int right, int qleft, int qright) {
	if (qleft > right || qright < left) return 0;
	if (qleft <= left && qright >= right) return value[pos];
	int mid = (left + right) / 2;
	return sum_range_segment_tree(value, pos * 2 + 1,
		left, mid, qleft, qright)
		+ sum_range_segment_tree(value, pos * 2 + 2,
			mid + 1, right, qleft, qright);
}

void update_segment_tree(vector<int>&value,
	int pos, int left, int right, int index, int new_value) {
	if (left == right && left == index) {
		value[pos] = new_value;
		return;
	}

	int mid = (left + right) / 2;
	if (index <= mid) {
		update_segment_tree(value, pos * 2 + 1, left, mid, index, new_value);
	}
	else {
		update_segment_tree(value, pos * 2 + 2, mid + 1, right, index, new_value);
	}

	value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
}

int main(int argc, char * argv[]) {
	vector<int>nums;
	for (int i = 0; i < 6; i++) {
		nums.push_back(i);
	}
	vector<int>value;
	for (int i = 0; i < 24; i++) {
		value.push_back(0);
	}
	build_segment_tree(value, nums, 0, 0, nums.size() - 1);
	printf("segment_tree:\n");
	print_segment_tree(value, 0, 0, nums.size() - 1, 0);
	int sum_range = sum_range_segment_tree(value, 0, 0, nums.size() - 1, 2, 4);
	printf("sum_range [2, 5]=%d\n", sum_range);
	update_segment_tree(value, 0, 0, nums.size() - 1, 2, 10);
	printf("segment_tree:\n");
	print_segment_tree(value, 0, 0, nums.size() - 1, 0);
	return 0;
}

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

 

 

 

leetcode 307 区域和检索 – 数组可修改

给定一个整数数组  nums,求出数组从索引 到 j  (i ≤ j) 范围内元素的总和,包含 i,  j 两点。

update(i, val) 函数可以通过将下标为 的数值更新为 val,从而对数列进行修改。

示例:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)          //把3改成2
sumRange(0, 2) -> 8

说明:

  1. 数组仅可以在 update 函数下进行修改。
  2. 你可以假设 update 函数与 sumRange 函数的调用次数是均匀分布的。

思路:

使用线段树,详细过程见上方

class NumArray {
public:
    void build_segment_tree(vector<int>&value,
                            vector<int>&nums, int pos, int left, int right) {
        if (left == right) {
            value[pos] = nums[left];
            return;
        }
        int mid = (left + right) / 2;
        build_segment_tree(value, nums, pos * 2 + 1, left, mid);
        build_segment_tree(value, nums, pos * 2 + 2, mid + 1, right);
        value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
    }
    
    int sum_range_segment_tree(vector<int>&value,
                               int pos, int left, int right, int qleft, int qright) {
        if (qleft > right || qright < left) return 0;
        if (qleft <= left && qright >= right) return value[pos];
        int mid = (left + right) / 2;
        return sum_range_segment_tree(value, pos * 2 + 1,
                                      left, mid, qleft, qright)
            + sum_range_segment_tree(value, pos * 2 + 2,
                                     mid + 1, right, qleft, qright);
    }
    
    void update_segment_tree(vector<int>&value,
                             int pos, int left, int right, int index, int new_value) {
        if (left == right && left == index) {
            value[pos] = new_value;
            return;
        }
        
        int mid = (left + right) / 2;
        if (index <= mid) {
            update_segment_tree(value, pos * 2 + 1, left, mid, index, new_value);
        }
        else {
            update_segment_tree(value, pos * 2 + 2, mid + 1, right, index, new_value);
        }
        
        value[pos] = value[pos * 2 + 1] + value[pos * 2 + 2];
    }
    
    NumArray(vector<int> nums) {
        if(nums.size()==0) return;
        //一般线段树的大小是原始数组大小的4倍
        int n=nums.size()*4;
        for(int i=0; i<n; i++){
            _value.push_back(0);
        }
        build_segment_tree(_value, nums, 0, 0, nums.size()-1);
        _right_end=nums.size()-1;
    }
    
    void update(int i, int val) {
        update_segment_tree(_value, 0, 0, _right_end, i, val);
    }
    
    int sumRange(int i, int j) {
        return sum_range_segment_tree(_value, 0, 0, _right_end, i, j);
    }
private:
    vector<int>_value;
    int _right_end;
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */

《分门别类刷leetcode——高级数据结构(字典树,前缀树,trie树,并查集,线段树)》

    原文作者:Trie树
    原文地址: https://blog.csdn.net/qq_29996285/article/details/86674779
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞