如何编写完整优化的SuffixTree代码
发表于:2025-11-10 作者:千家信息网编辑
千家信息网最后更新 2025年11月10日,本篇文章为大家展示了如何编写完整优化的SuffixTree代码,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。一些朋友提醒,这次一次放出实现了Ukkonen 的
千家信息网最后更新 2025年11月10日如何编写完整优化的SuffixTree代码
本篇文章为大家展示了如何编写完整优化的SuffixTree代码,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
一些朋友提醒,这次一次放出实现了Ukkonen 的paper的三个优化的完整SuffixTree的代码。看了之前博客的只实现SuffixLink优化的代码,再看这个应该就很简单了。
下面是SuffixTree的头文件SuffixTree.h
#pragma once#include#include using namespace std;class SuffixNode{public: vector m_pSons; SuffixNode* m_pFarther; SuffixNode* m_pSuffixLink; int m_iPathPos; int m_iEdgeStart; int m_iEdgeEnd;};class SuffixTree{public: int m_iE;//The virtual end of all leaves. SuffixNode* m_pRoot;//The root of the tree. string m_czTreeStr;//the string that the tree represent.};//Means a sub string of the suffix tree (string[beging],string[end]). class TreePath{public: int m_iBegin; int m_iEnd;};//Represent the char in a node's incoming edge.class TreePos{public: TreePos() { m_iEdgePos = 0; m_pNode = NULL; } TreePos(int edgePos,SuffixNode* pNode) { m_iEdgePos = edgePos; m_pNode = pNode; } int m_iEdgePos;//The ith char of the incoming edge. SuffixNode* m_pNode;//The node we are going to search.};//=====================================Class Declarations============================== void SingleCharExtesion(SuffixTree* pTree,TreePos*& pPos ,TreePath extendStrPath,int* firstExtensionFlag,bool * rule3Applied,int* iLeafNum);/* Add s[0....i+1],s[1...i+1].... to the suffix tree Input: SuffixNode* pNode : When we only use trick 1,pNode is the pointer to the longest leaf,s[0........i]. phase : Equals i+1 in the paper.*/void SinglePhaseExtend(SuffixTree* pTree,TreePos* & pPos,int phase,int* iExtension,int* ruleApplied,bool* lastRule3Applied,int *iLeafNum);SuffixNode* CreateTreeNode(SuffixNode* pFarther,int iedgeStart,int iedgeEnd,int pathPos);/* FollowSuffixLink : Follows the suffix link of the source node according to Ukkonen's rules(jump from s[j-1...i] to s[j....i]). Input : The tree, and node. The node is the last internal node we visited. Output: The destination node that represents the longest suffix of node's path. Example: if node represents the path "abcde" then it returns the node that represents "bcde".*/void FollowSuffixLink(SuffixTree* pTree,TreePos*& pPos, TreePath strji);int GetNodeLabelLength(SuffixTree* pTree, SuffixNode* pNode);int GetNodeLabelEnd(SuffixTree* pTree,SuffixNode* pNode);/* Find the son node which starts with the char,ch.*/SuffixNode* Find_Son(SuffixTree* pTree,SuffixNode* pFarNode, char ch);bool IsTheLastCharInEdge(SuffixTree* pTree, SuffixNode* pNode, int edge_pos);SuffixNode* ApplyExtensionRule2(SuffixNode* pNode,int edgeLabelBeg,int edgeLabelEnd,int edgePos,int pathPos,bool newLeafFlag);/* Trace the sub string(TreePath str) from the node(SuffixNode* pNode). Input: int* edgePos : where the last char is found at that edge int* charsFound : how many chars of str have been found. bool skipFlag : Use skip trick or not. */SuffixNode* TraceString(SuffixTree* pTree,SuffixNode* pNode,TreePath str,int* edgePos,int* charsFound,bool skipFlag);/* Trace the substring(TreePath strPath) in one single edge out of pNode.*/SuffixNode* TraceSingleEdge(SuffixTree* pTree,SuffixNode* pNode,TreePath strPath,int* charsFound,int* edgePos,bool* searchDone,bool skipFlag);SuffixNode* CreateFirstCharacter(SuffixTree* pTree);//Add the first character to the suffix tree.SuffixTree* CreateSuffixTree(string tStr);/*For Debug:See if the sub string (from root to pPos) equals pTree->string[subPath.m_iBegin,subPath.m_iEnd]*/bool TestPosSubStringEqualPath(SuffixTree* pTree,TreePos *pPos, TreePath subPath);/* For debugging, We want to see if the suffix link from linkFrom to linkTo is right.*/bool TestSuffixLinkMatch(SuffixTree *pTree, SuffixNode* linkFrom,SuffixNode *linkTo);int FindSubString(SuffixTree* pTree,string subStr);//=====================================================================================
下面是SuffixTree的实现文件SuffixTree.cpp
#pragma once#include "SuffixTree.h"#includeusing namespace std;SuffixNode* pNodeNoSuffixLink=NULL;//=====================================Class Definitions==============================/* Trace the substring(TreePath strPath) in one single edge going out of pNode. Input: int* edgeCharsFound : how many characters we find matched in the outgoing edge of pNode.*/SuffixNode* TraceSingleEdge(SuffixTree* pTree,SuffixNode* pNode,TreePath strPath,int* edgeCharsFound,int* edgePos,bool* searchDone,bool skipFlag){ //Find outgoing edge of pNode with our first character. SuffixNode* nextNode = Find_Son(pTree,pNode,pTree->m_czTreeStr[strPath.m_iBegin]); *searchDone = true; if(nextNode == NULL) {//There is no match in pNode's sons,so we can only return to pNode. *edgePos = GetNodeLabelLength(pTree,pNode); *edgeCharsFound = 0; return pNode; } int edgeLen = GetNodeLabelLength(pTree,nextNode); int strLen = strPath.m_iEnd - strPath.m_iBegin + 1; if(skipFlag == true)//Use the trick1 : skip { if(edgeLen < strLen) { *searchDone = false; *edgeCharsFound = edgeLen; *edgePos = edgeLen - 1; } else if(edgeLen == strLen) { *edgeCharsFound = edgeLen; *edgePos = edgeLen - 1; } else { *edgeCharsFound = strLen; *edgePos = strLen - 1; } return nextNode; } else//No skip,match each char one after another { *edgePos = 0; *edgeCharsFound = 0; //Find out the min length if(strLen < edgeLen) edgeLen = strLen; for(*edgeCharsFound=1,*edgePos=1;(*edgePos) m_czTreeStr[ nextNode->m_iEdgeStart + *edgePos ] != pTree->m_czTreeStr[strPath.m_iBegin + *edgePos ]) { (*edgePos)--; return nextNode; } } } //When it comes here, (*edgePos) is one more; (*edgePos)--; if(*edgeCharsFound < strLen) { *searchDone = false; } return nextNode;}/* Trace the sub string(TreePath str) from the node(SuffixNode* pNode). Input: int* edgePos :For output , where the last char is found at that edge int* charsFound : How many chars of str have been found. bool skipFlag : Use skip trick or not. */SuffixNode* TraceString(SuffixTree* pTree,SuffixNode* pNode,TreePath str,int* edgePos,int* charsFound,bool skipFlag){ bool searchDone=false; *charsFound = 0; *edgePos=0 ; int edgeCharsFound=0; while(searchDone==false) { edgeCharsFound = 0; *edgePos=0; pNode = TraceSingleEdge(pTree,pNode,str,&edgeCharsFound,edgePos,&searchDone,skipFlag); str.m_iBegin += edgeCharsFound; *charsFound += edgeCharsFound; } //if(*charsFound == 0) // return NULL; return pNode;}/* Input: (1) pNode : the node who is going to add a new son or whose edge is going to be split. (2) edgeLabelBeg : when newleafFlag==true,it's the edge begin label of the new leaf. when when newleafFlag==false, it's the edge begin label of the new new leaf( the leaf of s[i+1], not s[i]). (3) like above : just the end (4 )int edgePos : where split is done to pNode if newLeafFlag==false (the 0th position or 1th position or...)*/SuffixNode* ApplyExtensionRule2(SuffixNode* pNode,int edgeLabelBeg,int edgeLabelEnd,int edgePos,int pathPos,bool newLeafFlag){ if(newLeafFlag==true) { //Add an new leaf SuffixNode* newLeaf = CreateTreeNode(pNode,edgeLabelBeg,edgeLabelEnd,pathPos); return newLeaf; } else { //Add an new internal node and an new leaf //First create the new internal node. SuffixNode* nInternalNode = CreateTreeNode(pNode->m_pFarther,pNode->m_iEdgeStart,pNode->m_iEdgeStart + edgePos,pNode->m_iPathPos); //Remove pNode from its farther's sons for(vector ::iterator pNodeIter=pNode->m_pFarther->m_pSons.begin(); pNodeIter!=pNode->m_pFarther->m_pSons.end();pNodeIter++) { if( pNode == *pNodeIter ) { pNode->m_pFarther->m_pSons.erase(pNodeIter); break; } } //Adjust pNode's information. pNode->m_iEdgeStart += (edgePos + 1); pNode->m_pFarther = nInternalNode; nInternalNode->m_pSons.push_back(pNode); //Create the new leaf for s[i+1] SuffixNode* nLeafNode = CreateTreeNode(nInternalNode,edgeLabelBeg,edgeLabelEnd,pathPos); return nInternalNode; }}bool IsTheLastCharInEdge(SuffixTree* pTree, SuffixNode* pNode, int edge_pos){ if( edge_pos == GetNodeLabelLength(pTree,pNode) - 1 ) return true; return false;}int GetNodeLabelEnd(SuffixTree* pTree,SuffixNode* pNode){ if(pNode->m_pSons.size() == NULL) { return pTree->m_iE; } return pNode->m_iEdgeEnd;}int GetNodeLabelLength(SuffixTree* pTree, SuffixNode* pNode){ int length = GetNodeLabelEnd(pTree,pNode) - pNode->m_iEdgeStart + 1; return length;}SuffixNode* CreateTreeNode(SuffixNode* pFarther,int iedgeStart,int iedgeEnd,int pathPos){ SuffixNode* pNode=new SuffixNode(); pNode->m_iEdgeStart = iedgeStart; pNode->m_iEdgeEnd = iedgeEnd; pNode->m_pFarther = pFarther; pNode->m_iPathPos = pathPos; pNode->m_pSuffixLink = NULL; if(pFarther!=NULL) pFarther->m_pSons.push_back(pNode); return pNode;}//Find the son node which starts with the chSuffixNode* Find_Son(SuffixTree* pTree,SuffixNode* pFarNode, char ch){ for(vector ::iterator nodeIter=pFarNode->m_pSons.begin(); nodeIter!=pFarNode->m_pSons.end();nodeIter++) { if(pTree->m_czTreeStr[(*nodeIter) -> m_iEdgeStart] == ch ) { return *nodeIter; } } return NULL;}/* FollowSuffixLink : Follows the suffix link of the source node according to Ukkonen's rules(jump from s[j-1...i] to s[j....i]). Input : The tree, and node. The node is the last internal node we visited. Output: The destination node that represents the longest suffix of node's path. Example: if node represents the path "abcde" then it returns the node that represents "bcde".*/void FollowSuffixLink(SuffixTree* pTree,TreePos* & pPos,TreePath strji){ /*gama : the string(r in Gusfield's paper) between node and its father. If the node doesn't have suffix link , we need to go up to its farther*/ TreePath gama; if(pPos->m_pNode == pTree->m_pRoot) { pPos->m_iEdgePos = 0; return; } // No suffix link,walk up at most one step(if it is not the root). if( pPos->m_pNode->m_pSuffixLink == NULL ) { if(pPos->m_pNode->m_pFarther == pTree->m_pRoot) {//its farther is the root pPos->m_pNode = pTree->m_pRoot; pPos->m_iEdgePos = 0; return; } else { // Find the gamma (the substring between pPos's parent's and pPos's) gama.m_iBegin = pPos->m_pNode->m_iEdgeStart; gama.m_iEnd = pPos->m_pNode->m_iEdgeStart + pPos->m_iEdgePos;// the end of s[j..i] //Follow farther's suffix link pPos->m_pNode = pPos->m_pNode->m_pFarther->m_pSuffixLink; //Down-walk gamma (until we found s[i],the character we add last extension) int charsFound=0; pPos->m_pNode = TraceString(pTree,pPos->m_pNode,gama,&pPos->m_iEdgePos,&charsFound,true); //////////////////////////////////////////////// } } else { //A suffix link exists - just follow it. pPos->m_pNode = pPos->m_pNode->m_pSuffixLink; pPos->m_iEdgePos = GetNodeLabelLength(pTree,pPos->m_pNode) - 1; //The last char of pPos's suffix link represents s[i] (the character we add last extension). } return;}/*For Debug:See if the sub string (from root to pPos) equals pTree->string[subPath.m_iBegin,subPath.m_iEnd]*/bool TestPosSubStringEqualPath(SuffixTree* pTree,TreePos *pPos, TreePath subPath){ if(pTree->m_pRoot == pPos->m_pNode ) { return true; } int strRevIndex = subPath.m_iEnd; SuffixNode* tmpNode = pPos->m_pNode; int edgeRevIndex = tmpNode->m_iEdgeStart + pPos->m_iEdgePos; while(tmpNode != pTree->m_pRoot) { while( edgeRevIndex >= tmpNode->m_iEdgeStart && strRevIndex >= subPath.m_iBegin) { if( pTree->m_czTreeStr[edgeRevIndex] != pTree->m_czTreeStr[strRevIndex] ) { return false; } edgeRevIndex--; strRevIndex--; } tmpNode = tmpNode->m_pFarther; edgeRevIndex = tmpNode->m_iEdgeEnd; } if(strRevIndex != subPath.m_iBegin-1) return false; return true;}/*Input:(1) SuffixTree* pTree : The suffix tree(2) TreePos* pPos : The last internal node we visited , then we are going to jump to its suffix link in this extension.(3) TreePath extendStrPath : The suffix (s[j...i+1]) we are goint to add to the tree.*/void SingleCharExtesion(SuffixTree* pTree,TreePos*& pPos ,TreePath extendStrPath,int* ruleApplied,bool* lastRule3Applied,int *iLeafNum){ TreePath sji; sji.m_iBegin = extendStrPath.m_iBegin; sji.m_iEnd = extendStrPath.m_iEnd - 1; int pathPos = extendStrPath.m_iBegin; if(*lastRule3Applied == false) { //Ready to jump from suffix link at or above s[j-1...i] that either has a suffix link to s[j...i] or the root . FollowSuffixLink(pTree,pPos,sji); } else { *lastRule3Applied = false; } //////////////////////////////////////////For Debug////////////////////////////////////////////////// if(sji.m_iEnd >= sji.m_iBegin) { if(TestPosSubStringEqualPath(pTree,pPos, sji) == false && pPos->m_pNode != pTree->m_pRoot) { cout<<"FollowSuffixLink doesn't go to the right s[j..i]:"< m_pNode == pTree->m_pRoot && sji.m_iEnd >= sji.m_iBegin) { pPos->m_pNode = TraceString(pTree,pTree->m_pRoot,sji,&pPos->m_iEdgePos,&chars_found,false); if(pPos->m_pNode == NULL) { pPos->m_iEdgePos = 0; pPos->m_pNode =pTree->m_pRoot; //////////////////////////////////For Debug////////////////////////////////////// if(sji.m_iBegin != sji.m_iEnd) { cout<<"There is s[j-1..i](not empty) doesn't exist!"< m_pNode,pPos->m_iEdgePos)) { SuffixNode* pTmp = Find_Son(pTree,pPos->m_pNode,pTree->m_czTreeStr[extendStrPath.m_iEnd]); if(pTmp != 0) { //s[i+1] exits already. chars_found = 1; *ruleApplied = 3; //We found s[i+1] which is already in the tree. pPos->m_iEdgePos = 0; pPos->m_pNode = pTmp; if(pNodeNoSuffixLink != NULL) { pNodeNoSuffixLink->m_pSuffixLink = pPos->m_pNode->m_pFarther;//Let s[j-1..i] jump to s[j...i] ///////////////////////////For Debug////////////////////////////////////////////////////////////////// if(!TestSuffixLinkMatch(pTree,pNodeNoSuffixLink ,pNodeNoSuffixLink->m_pSuffixLink)) { cout<<"Suffix Tree Build Error"< m_czTreeStr[ pPos->m_pNode->m_iEdgeStart + pPos->m_iEdgePos + 1] == pTree->m_czTreeStr[extendStrPath.m_iEnd] )//Notice that " + 1 " means the next char of s[j...i] : yes, s[i+1] {//s[i+1] exits already. chars_found = 1; if(pPos->m_pNode->m_pSons.size() == 0)//We found s[i+1] in the leaf { if( pPos->m_iEdgePos + 1 == GetNodeLabelLength(pTree,pPos->m_pNode) - 1) //Rule1 applied, we just add s[i+1] to the leaf. { *ruleApplied = 1; } else { *ruleApplied = 3; pPos->m_iEdgePos++;//Go to the s[i+1] } } else//We found s[i+1] in the internal node { *ruleApplied = 3; pPos->m_iEdgePos++;//Go to s[i+1] if(pNodeNoSuffixLink != NULL) { pNodeNoSuffixLink->m_pSuffixLink = pPos->m_pNode; ///////////////////////////For Debug////////////////////////////////////////////////////////////////// if(! TestSuffixLinkMatch(pTree,pNodeNoSuffixLink ,pNodeNoSuffixLink->m_pSuffixLink)) { cout<<"Suffix Tree Build Error"< m_pNode,pPos->m_iEdgePos) || pPos->m_pNode==pTree->m_pRoot) { if(pPos->m_pNode->m_pSons.size() != 0) { //Internal node or root,apply rule2 that add a new leaf ApplyExtensionRule2(pPos->m_pNode, extendStrPath.m_iEnd, extendStrPath.m_iEnd, 0, pathPos, true); //Suffix Link if(pNodeNoSuffixLink != NULL) { pNodeNoSuffixLink->m_pSuffixLink = pPos->m_pNode; ///////////////////////////For Debug////////////////////////////////////////////////////////////////// if(! TestSuffixLinkMatch(pTree,pNodeNoSuffixLink ,pNodeNoSuffixLink->m_pSuffixLink)) { cout<<"Suffix Tree Build Error"< m_pNode,extendStrPath.m_iEnd,extendStrPath.m_iEnd,pPos->m_iEdgePos,pathPos,false); if(pNodeNoSuffixLink != NULL) { pNodeNoSuffixLink->m_pSuffixLink = nInternalNode; ///////////////////////////For Debug////////////////////////////////////////////////////////////////// if(! TestSuffixLinkMatch(pTree,pNodeNoSuffixLink ,pNodeNoSuffixLink->m_pSuffixLink)) { cout<<"Suffix Tree Build Error"< m_pFarther == pTree->m_pRoot) { nInternalNode->m_pSuffixLink = pTree->m_pRoot; pNodeNoSuffixLink = NULL; } else { pNodeNoSuffixLink = nInternalNode; } //Adjust the node for the next extension pPos->m_pNode = nInternalNode; (*iLeafNum)++; *ruleApplied = 2; } return ;}/* Add s[0....i+1],s[1...i+1].... to the suffix tree Input: SuffixNode* pNode: When we only use trick 1,pNode is the pointer to the longest leaf,s[0........i].*/void SinglePhaseExtend(SuffixTree* pTree,TreePos* & pPos,int phase,int* iExtension,int* ruleApplied,bool *lastRule3Applied,int *iLeafNum){ //pTree->m_iE = phase-1; //int ruleApplied=-1; while(*iExtension <= phase ) { TreePath extendPath; extendPath.m_iBegin = *iExtension; extendPath.m_iEnd = phase; SingleCharExtesion(pTree,pPos,extendPath,ruleApplied,lastRule3Applied,iLeafNum); if(*ruleApplied == 3)//We already found s[j...i+1] which means s[j+1...i+1] and the rest are already in the tree too. { *lastRule3Applied = true; break; } (*iExtension)++; } return;}SuffixNode* CreateFirstCharacter(SuffixTree* pTree){ pTree->m_iE = 0; SuffixNode* firstLeaf = CreateTreeNode(pTree->m_pRoot,0,0,0); return firstLeaf;}SuffixTree* CreateSuffixTree(string tStr){ SuffixTree* psTree=new SuffixTree(); psTree->m_czTreeStr = tStr+"$"; psTree->m_pRoot = CreateTreeNode(NULL,0,0,0); //Add the first char into it. SuffixNode* firstLeaf = CreateFirstCharacter(psTree); TreePos* lastLeafPos = new TreePos(0,firstLeaf); int ruleApplied = 2; int iExtension = 1; int iLeafNum = 1; bool lastRule3Applied = false; for(int phase = 1 ; phase m_czTreeStr.length() ; phase++) { psTree->m_iE = phase; iExtension = iLeafNum; //lastLeafPos->m_iEdgePos = lastLeafPos->m_pNode->m_iEdgeEnd - lastLeafPos->m_pNode->m_iEdgeStart; //start from s[j..i] SinglePhaseExtend(psTree,lastLeafPos,phase,&iExtension,&ruleApplied,&lastRule3Applied,&iLeafNum); } return psTree;}int FindSubString(SuffixTree* pTree,string subStr){ SuffixNode* node = Find_Son(pTree,pTree->m_pRoot,subStr[0]); if(node == NULL) { return -1; } int startIndex = node->m_iEdgeStart; int strIndex=0; int edgeIndex; while(node != NULL) { edgeIndex = node->m_iEdgeStart; int edgeLabelEnd = GetNodeLabelEnd(pTree,node); while(strIndex < subStr.length() && edgeIndex <= edgeLabelEnd && pTree->m_czTreeStr[edgeIndex] == subStr[strIndex]) { strIndex++; edgeIndex++; } if(strIndex == subStr.length()) { //we found it return node->m_iPathPos; } else if(edgeIndex > node->m_iEdgeEnd) { node = Find_Son(pTree,node,subStr[strIndex]); } else { return -1; } } return -1;}/* For debugging, We want to see if the suffix link from linkFrom to linkTo is right.*/bool TestSuffixLinkMatch(SuffixTree *pTree, SuffixNode* linkFrom,SuffixNode *linkTo){ char ch2,ch3; while(linkFrom->m_pFarther->m_pFarther!=pTree->m_pRoot && linkFrom->m_pFarther != pTree->m_pRoot) { linkFrom = linkFrom->m_pFarther; } if(linkFrom->m_pFarther == pTree->m_pRoot) { if(linkFrom->m_iEdgeEnd == linkFrom->m_iEdgeStart) { if(linkTo != pTree->m_pRoot) { return false; } else { return true; } } else { ch2 = pTree->m_czTreeStr[linkFrom->m_iEdgeStart + 1]; } } else { if(linkFrom->m_pFarther->m_iEdgeStart == linkFrom->m_pFarther->m_iEdgeEnd) { ch2 = pTree->m_czTreeStr[linkFrom->m_iEdgeStart]; } else { ch2 = pTree->m_czTreeStr[linkFrom->m_pFarther->m_iEdgeStart + 1]; } } while(linkTo->m_pFarther != pTree->m_pRoot) { linkTo = linkTo->m_pFarther; } ch3 = pTree->m_czTreeStr[linkTo->m_iEdgeStart]; if(ch2 == ch3) return true; else return false;}
最后是调用的主函数,一个简单的判断子串是否在字符串中,如果子串subStr存在于字符创str中,输出它的起点,否则输出不存在。
#include "SuffixTree.h"#include#include using namespace std;int main(){ string str="avnaoihvnjovsdvaeveavsfvwevfwa vafjoajv aoja aojfoaj afowajop afoajv afjoajo csvweavefvs fjwojvwoajv haovpjvowj ajovwjavo aojvowajv ajvoajv vnaojv vfdvdfvavaewvavaisadnvioenvoehnvPIavnaoihvnjovsdvaeveavsfvwevfwa vafjoajv aoja aojfoaj afowajop afoajv afjoajo csvweavefvs fjwojvwoajv haovpjvowj ajovwjavo aojvowajv ajvoajv vnaojv vfdvdfvavaewvavaisadnvioenvoehnvPI"; string subStr=" vafjoajv aoja aojfoaj afowajop afoajv afjoajo csvweavefvs fjwojvwoajv haov"; SuffixTree* pTree = CreateSuffixTree(str); int existFlag = FindSubString(pTree,subStr); if(existFlag>=0) cout<<"Sub string starts from "< 上述内容就是如何编写完整优化的SuffixTree代码,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注行业资讯频道。
代码
内容
字符
技能
文件
知识
输出
简明
简明扼要
三个
函数
博客
字符串
就是
文章
更多
朋友
果子
篇文章
行业
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
前端部署服务器的哪个文件
hp服务器虚拟化价格
拨号上网用的什么网络技术
慧博投研资讯数据库
服务器ups
打印机远程服务器如何开启
r210服务器管理口
网络安全竞赛试题答案
论文数据库授权书
江苏科技院网络技术有限公司
石景山区专业性软件开发职责
数据库应用技术作业形考
网络安全公司有多缺人
数据库设计两个表
软件开发测试环境搭建
发动机应用层软件开发
文件服务器怎么用
服务器算力指标
服务器 远程管理 曙光
mysql连接外网数据库
java连接sql数据库
炫拓网络技术服务部 假
长沙图书馆数据库
手机端http服务器软件
网络安全宣传周小游戏
支付宝的云服务器中国哪里呀
梦云服务器
服务器登录设备管理器
hp服务器 拆机
试述电子商务网络安全要素