【练习】2021下半年数据结构刷题笔记和总结 (二) 树、查找-- 不同的排序算法、二叉排序树 平衡二叉树、哈希表查找、线索二叉树、
記錄自己下半年寫題目的記錄。題目來自書或者網(wǎng)站。
練習(一)的地址:
https://blog.csdn.net/qq_41358574/article/details/117098620?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162343250816780357289113%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=162343250816780357289113&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_v2~rank_v29-2-117098620.nonecase&utm_term=%E5%88%B7%E9%A2%98&spm=1018.2226.3001.4450
文章目錄
- 知識點整理
- 1.一棵二叉排序樹,設計算法查找兩個結點之和等于給定值x的結點
- 2.樹的層次遍歷
- 3.一二叉排序樹中的關鍵字由整數(shù)構成,為了查找某關鍵字k,會得到一個查找序列,判斷一個數(shù)組a中的序列是否為該二叉排序的查找序列
- 4.求關鍵字分別為x,y的結點的最近公共祖先
- 5.設計算法將二叉排序樹變?yōu)橛行蜴湵?#xff0c;要求不能創(chuàng)建新結點,只修改指針
- 6.設計算法交換二叉樹b的所有左右子樹要求空間復雜度為1
- 7.設計算法交換二叉樹b的所有左右子樹要求不破壞原二叉樹結構
- 8.設計算法利用結點的右孩子指針將一棵二叉樹的葉結點從左往右串成單鏈表
- 9.設計算法輸出所有根結點到葉結點的路徑及其路徑和
- 10.【面試題9-11★★★】一個含有13個元素的數(shù)組為前半部分、后半部分是遞增有序,但整個數(shù)組不是遞增數(shù)組,且不知道前、后兩個部分中的元素個數(shù),怎么最快地找出其中的一個數(shù)
- 11.一個非降序數(shù)組,若target在數(shù)組中出現(xiàn),返回位置,否則返回它將插入的位置
- 12.二分插入排序
- 13.希爾排序
- 14.桶排序實戰(zhàn):用鏈表表示桶,從文件中讀取數(shù)字
- 15.一個序列用帶頭結點的單鏈表存儲,采用快速排序求遞增排序
- 16.二叉查找樹的操作
- 遍歷操作:
- 插入結點
- 二叉查找樹的刪除操作
- 二叉查找樹的查找操作
- 判斷一個數(shù)組是否是二叉查找樹的后序遍歷
- 17.線索二叉樹
- 數(shù)據(jù)結構如下:
- 尋找中序遍歷線索二叉樹的前驅和后繼結點
- 先序、后序遍歷的前驅后繼
- 線索二叉樹中插入結點
- . 二叉樹的線索化算法實現(xiàn)
- 遍歷線索二叉樹
- 18.分塊查找
知識點整理
- 平衡二叉樹插入結點時速度比較慢
- 平衡二叉樹的各項操作的時間復雜度為o(logn)
- 有序數(shù)組的查找性能高但刪除性能低,有序鏈表的查找性能低但刪除性能高,AVL和哈希表的查找和刪除性能都高
1.一棵二叉排序樹,設計算法查找兩個結點之和等于給定值x的結點
1.思路一 中序遍歷產(chǎn)生中序序列(左子樹所有結點的關鍵字均小于根節(jié)點關鍵字)
typedef struct node {char data;struct node* lchild;struct node* rchild; }btnode; void InOrder(btnode* bt, vector<btnode*>& order) {//order:中序遍歷產(chǎn)生的序列InOrder(bt->lchild, order);order.push_back(bt);InOrder(bt->rchild, order); } bool FindSum(vector<btnode*>order, int x, btnode* &p1, btnode* &p2)//p1 p2傳入的是引用 這樣能得到和為x的結點 {//查找兩個結點值之和等于x的p1和p2int i = 0, j = order.size()-1;while (i < j){if (order[i]->data + order[j]->data == x) {p1 = order[i];p2 = order[j];return true;}else if (order[i]->data + order[j]->data > x){j--;}elsei++;}return false; }bool TwoNodeSum(btnode* bt, int x, btnode*& p1, btnode*& p2) {vector<btnode*>order;InOrder(bt, order);if (FindSum(order,x, p1, p2)) return true;else return false; }思路二 采用兩個棧分別記錄bt的左下結點和右下結點(初始狀態(tài)保存根結點的左下結點和右下結點)
bool TwoNodeSum(btnode* bt, int x, btnode*& p1, btnode*& p2) {stack<btnode*>left, right;btnode* p = bt, * q;while (p != NULL) {if (p->lchild != nullptr) {left.push(p->lchild); p = p->lchild;}p = bt;//注意這個步驟 此后根結點再次賦給pif (p->rchild != nullptr) {right.push(bt->rchild);p = p->rchild;}while (!left.empty() && !right.empty()){p1 = left.top();p2 = right.top();if (p1->data + p2->data > x)//找次大結點 即p2左孩子的所有右下結點進棧{q = p2;right.pop();if (q->lchild != NULL){p = q->lchild;while (p) {right.push(p);p = p->rchild;}}}else if (p1->data + p2->data < x){//找次小結點q = p1;left.pop();if (q->rchild != NULL){p = q->rchild;while (p){left.push(p);p = p->lchild;}}}else return true;}};return false; }2.樹的層次遍歷
按從上到下從左到右的順序輸出各個結點的值,如果從根到某個葉節(jié)點的路徑上有的結點沒有在輸入中給出,或者給出超過一次,應該輸出-1
樣例輸入:
(11, LL) (7, LLL) (8, R) (5, ) (4, L) (13, RL) (2, LLR) (1, RRR) (4, RR) ()
輸出:
5 4 8 11 13 4 7 2 1
3.一二叉排序樹中的關鍵字由整數(shù)構成,為了查找某關鍵字k,會得到一個查找序列,判斷一個數(shù)組a中的序列是否為該二叉排序的查找序列
//假設序列a有n個關鍵字,如果查找成功則a[n-1]=k,用i掃描a數(shù)組,p用于在二叉樹bt中查找(p的初值指向根節(jié)點) 注意:沒查找一層都比較p->key與a[i]是否相等 //如果不相等 返回false bool FindSer(Node* bt, int k, int a[], int n) {if (bt == NULL) return false;if (a[n - 1] != k) return false;//先提前判斷這個Node* p = bt;int i = 0;while (i < n && p != NULL){if (p->val != a[i]) return false;if (k > p->val) p = p->right;else if (k < p->val) p = p->right;i++;}if (p != NULL) return true;return false; }4.求關鍵字分別為x,y的結點的最近公共祖先
Node* LCA(Node*bt,int x,int y) {{if (bt == nullptr) return NULL;if (x < bt->left->val && y < bt->left->val) return LCA(bt->left, x, y);else if (x > bt->right->val && y > bt->right->val) return LCA(bt->right, x, y);else return bt;//注意 如果以上兩種情況都不是,則bt為最近公共結點}5.設計算法將二叉排序樹變?yōu)橛行蜴湵?#xff0c;要求不能創(chuàng)建新結點,只修改指針
方法一:
//1.遞歸的方法 將二叉樹bt中所有結點的左指針lchild轉化為指向前驅結點,右指針轉化為指向后繼結點 //if bt->lchild==null first = bt,if bt->rchild==null last =bt //注意需要銷毀雙鏈表 void convert(Node* bt, Node*& first, Node*& last) {if (bt == nullptr) {//空樹first = nullptr;last = nullptr;}Node* lfirst, * llast, * rfirst, * rlast;//遞歸調用時左子樹和右子樹的首位指針,一共四個if (bt->left == nullptr) first = bt;else if (bt->left != nullptr) {convert(bt->left, lfirst, llast);first = lfirst;bt->left = llast;llast->right = bt;}if (bt->right == nullptr)last = bt;else {convert(bt->right, rfirst, rlast);last = rlast;bt->right = rfirst;rfirst->left = bt;} } void Display(Node* first, Node* last) {if (first != NULL) {//輸出轉化后的雙鏈表while (first != last){cout << first->val << ",";first = first->right;}cout << first->val;//最后一個結點的值}} void destroy(Node*& L) {//銷毀雙鏈表Node* pre = L, *p = pre->right;//一個首結點 一個首結點的后繼結點while (pre->right){free(pre);pre = p;p = pre->right;}free(pre);//注意最后還有一個結點要釋放 }方法二:
中序遍歷 用pre全局變量指向中序遍歷當前訪問結點bt的前驅節(jié)點,初始為NULL,中序訪問bt時,設置pre->rchild=bt bt->lchid = pre
兩個方法的輸出和銷毀雙鏈表函數(shù)是一樣的
6.設計算法交換二叉樹b的所有左右子樹要求空間復雜度為1
void swap(btnode*b) { btnode*temp; if(b!=NULL) { swap(b->lchild); swap(b->rchild); temp=b->lchild; b->lchild=b->rchild; b->rchild=temp; } } //基于后序遍歷算法7.設計算法交換二叉樹b的所有左右子樹要求不破壞原二叉樹結構
btnode* swap(btnode*b) { btnode*t,*t1,*t2; if(b==NULL)t=NULL; else { t=(btnode*)malloc(sizof(btnode));//復制根節(jié)點 t->data=b->data; t1=swap(b->lchild); t2=swap(b->rchild); t->lchild=t2; t->rchild=t1; } return t; }8.設計算法利用結點的右孩子指針將一棵二叉樹的葉結點從左往右串成單鏈表
//先序遍歷的方法,尾插法構建葉結點,head指向建立單鏈表的首結點,tail指向尾結點 //head tail以引用的方式傳入 void link(btnode*b,btnode*&head,btnode*&tail) { if(b!=NULL) { if(b->lchild==NULL&&b->rchild==NULL)//葉結點 { if(head==NULL){//此時為第一個葉結點 head=b; tail=head; } else { tail->rchild=b; tail=b; } } if(b->lchild!=NULL){ link(b->lchild,head,tail); } if(b->rchild!=NULL)link(b->rchild,head,tail); } //創(chuàng)建并輸出葉結點構成的單鏈表 void vreateLink(btnode*b) { btnode*head=NULL,*tail; link(b,head,tail)//注意head一開始指向null tail->rchild=NULL;//注意尾結點的rchild置空 btnode*p=head; while(p!=NULL) { cout<<p->data<<","; p=p->next; }}9.設計算法輸出所有根結點到葉結點的路徑及其路徑和
void dispapath(vector<int>path) { vector<int>::iterator it; int sum=0; for(it=path.begin();it!=path.end();it++) { sum+=*it; cout<<*it<<" "; } cout<<sum;void pathSum(btnode*b,vector<int>path) { if(b==NULL)return; path.push_back(b->data); if(b->lchild==NULL&&b->rchild==NULL) { dispath(path);//找到一個葉結點,輸出一條路徑 } pathSum(b->lchild,path); pathSum(b->rchild,path);//如果不是葉結點,先序遍歷的方式繼續(xù)尋找后面的結點10.【面試題9-11★★★】一個含有13個元素的數(shù)組為前半部分、后半部分是遞增有序,但整個數(shù)組不是遞增數(shù)組,且不知道前、后兩個部分中的元素個數(shù),怎么最快地找出其中的一個數(shù)
如[2,4,5,6,7,8][1,3,5,6,7]
解:由于不知道前、后兩個有序部分中的元素個數(shù),所以先要將其找出來。假設前、后兩個部分分別為a[0.m]和a[m+.n-1],先采用順序方法在a[0…m]中從前向后查找,若a[i]==target(目標元素),找到后算法結束,或者發(fā)現(xiàn)元素逆序后退出循環(huán)(即找到分界線
元素a[m]),然后在a[m+1.n-1]有序區(qū)間中采用二分查找
上述過程對應的時間復雜度為O(n)。對應的算法如下:
11.一個非降序數(shù)組,若target在數(shù)組中出現(xiàn),返回位置,否則返回它將插入的位置
寫法1:
int SearchInsert(int a[],int n,int target) {if(a[n-1] < target) return n;//a是一遞增數(shù)組int low=0,high = n-1;while(low <= high){int mid = (low+high)/2;if(a[mid] > target) high=mid-1;else if(a[mid] < target) low=mid+1;else return mid;}return low; }寫法2:
int SearchInsert(int a[],int n,int target) {if(a[n-1] < target) return n;//a是一遞增數(shù)組int low=0,high = n-1;while(low < high){int mid = (low+high)/2;if(a[mid] > target) high=mid;else low=mid+1;}return high; }12.二分插入排序
void BinInsertSort(int a[],int n) {int i,j,low,high,mid;int tmp;for(i = 1;i < n;i++){if(a[i] < a[i-1]){tmp = a[i];low=0;high=i-1;while(low<=high){mid = (low+high)/2;if(a[mid] > tmp) high=mid-1;else low=mid+1;}for(j=i-1;j>=high+1;j--)a[j+1]=a[j];//集中進行元素后移a[high+1]=tmp;}} }int main() {int a[5]={1,74,3,15,6}; BinInsertSort(a,5); for(int i = 0;i < 5;i++) cout<<a[i]<<" ";}13.希爾排序
void shell(int a[],int n) {int i,j,d;int tmp;d = n/2;while(d > 0){ for(i = d;i < n;i++) { j = i - d; tmp = a[i]; while(j>=0 && a[j] > tmp) {a[j+d] = a[j];j = j-d; } a[j+d] = tmp;}d = d/2;} }是不穩(wěn)定的排序算法。
每一趟不一定產(chǎn)生有序區(qū),最后一趟產(chǎn)生全部的有序序列。
直接插入排序:每一趟產(chǎn)生的有序區(qū)不一定是全局有序的。是穩(wěn)定的排序算法。平均時間復雜度為o(n2)
對于直接插入排序,初始數(shù)據(jù)越接近正序性能越好。
14.桶排序實戰(zhàn):用鏈表表示桶,從文件中讀取數(shù)字
15.一個序列用帶頭結點的單鏈表存儲,采用快速排序求遞增排序
先找劃分基準,再分成兩半遞歸排序
void swap(int&x,int &y) { int temp = x; x = y; y = temp; } LinkNode* Paration(LinkNode* first,LinkNode*tail) { if(first == tail) return first;//只有一個結點 LinkNode* pt = first,* p =pt->next; int temp = first->data;//基準值 while(true) { if(p->data < temp) { pt = pt->next; swap(pt->data,p->data);//如果p結點小于temp } if(p == tail) break; p = p->next; } swap(pt->data,first->data);//找到劃分位置,即pt指向的結點 return pt; } void QuickSort(LinkNode*&first,LinkNode*&tail) { if(first!=NULL && tail != NULL && first != tail) { LinkNode*pt = Paration(first,tail); QuickSort(first,pt); QuickSort(pt->next,tail); } }通過修改結點的值實現(xiàn),沒有改變結點地址。
16.二叉查找樹的操作
遍歷操作:
void inSort(btnode*node) { if(node!=NULL) { inSort(node->lchild);//遞歸遍歷左子樹 std::cout<<node->data<<std::endl;//輸出結點數(shù)據(jù) inSort(node->rchild);//遞歸遍歷右子樹 } }插入結點
步驟:如果二叉查找樹為空,則插入結點就為根節(jié)點
否則將待插入結點與根節(jié)點進行比較,如果小于根節(jié)點就插入到左子樹,大于根節(jié)點就插入到右子樹中
二叉查找樹的刪除操作
考慮幾種情況:
如果沒有子女結點,則修改父節(jié)點的指針,使其指向NULL,刪除結點x
如果結點x只有一個孩子結點,修改其父節(jié)點和孩子結點的parent指針 建立聯(lián)系,如果左右孩子都存在 用左子樹中的最大值或者右子樹的最小值代替x
二叉查找樹的查找操作
btnode search(DataType k) { btnode*node = root; while(node!=NULL) { if(node->data>k) node= node->lchild; else if(node->data < k) node = node->rchild; else break;//相等則退出 } return node; }判斷一個數(shù)組是否是二叉查找樹的后序遍歷
//判斷一個數(shù)組是否是一個二叉查找樹的后序遍歷 bool postSequence(int sequence[],int len) {if(sequence == NULL || len <= 0) return false;//先驗錯 int root = sequence[len-1];//找到根節(jié)點 int i = 0; for(;i<len-1;i++) if(sequence[i]>root) break;//左子樹均小于根節(jié)點 這里i是下標也是長度 int j = i; for(;j < len-1;j++)//注意這里j是下標而不是長度 if(sequence[j] < root) return false;//如果右子樹有小于根節(jié)點的 返回false bool left = true;//判斷左子樹是否是查找樹 if(i > 0) {//有可能沒有左子樹,故加上i>0的判斷left = postSequence(sequence,i);//i是左子樹的長度 } bool right = true; if(j < len-1) right = postSequence(sequence+i,len-i-1);//注意右子樹開始的序列和右子樹的長度 return (right && left); }int main() {int s[7];for(int i = 0;i <7;i++)cin>>s[i];bool res=postSequence(s,7);if(res)cout<<"是"; }5 7 6 9 11 10 8
17.線索二叉樹
我們利用二叉樹鏈式存儲結構的空指針來存儲結點的直接前驅指針和直接后繼結點指針,這些指針稱為線索,利用線索來保留前驅和后繼結點信息的二叉樹稱為線索二叉樹。
我們設定結點左指針指向前驅節(jié)點,右指針指向后繼結點,且每個結點增加兩個標志量lFlag和rFlag,當lchild rchild是指向孩子節(jié)點的指針時相應的標志量為1,是線索時為0
也就是說rchild lchild可能時指向孩子的指針也可能是線索
數(shù)據(jù)結構如下:
template<typename DataType>class ThreadBTNode{ public: friend class ThreadBT<DataType>; ThreadBTNode(DataType data) { lFlag = rFlag =0; this->data = data; lchild = rchild = NULL; } ThreadBTNode() { lFlag = rFlag =0; lchild = rchild = NULL;//帶數(shù)據(jù)結點的構造函數(shù)和空構造函數(shù) } private: ThreadBTNode<DataType>*lchild,*rchild;//左右指針域 int lFlag,rFlag;//標志量 DataType data;//數(shù)據(jù)域 }; template<typename DataType>class ThreadBT{ public: ThreadBT(){ root = NULL; } ThreadBT(DataType data) { root = new ThreadBTNode< DataType>(data); } private:}尋找中序遍歷線索二叉樹的前驅和后繼結點
線索二叉樹的前驅和后繼結點與使用的遍歷方式有關,線索二叉樹之間的區(qū)別就是根據(jù)不同的遍歷方式其結點的前驅和后繼不同
對二叉樹進行一次中序遍歷便明白結點node的前驅節(jié)點是左子樹中最右邊的結點,后繼結點是右子樹最左邊的結點
同理查找后繼:
template<typename DataType>ThreadBTNode<DataType>*ThreadBT<DataType>::succ(ThreadBTNode<DataType>*node) { ThreadBTNode<DataType>*s; s = node->rchild; if(node->rFlag == 1) { while(s->lFlag == 1)//循環(huán)查找結點的右子節(jié)點,找到右子樹的最左節(jié)點 s = s->lchild; } return s; }先序、后序遍歷的前驅后繼
標志ltag和rtag,并約定:
0 結點中的 lchild字段指向左孩子;
ltag=
1 結點中的 lchild字段指向前驅;
0 結點中的 rchild字段指向右孩子;
rtag=
1 結點中的 rchild字段指向后繼;
(1) 先序線索二叉樹中先序后繼的求解——先序后繼
對先序線索二叉樹中任意結點P,求其先序后繼。
討論:
(a) 若p有左孩子 ——按照遍歷的過程描述(PPLPR)可知,
其后繼應為:左子樹PL中的第一個結點,
即p的左孩子結點,
因此, p->lchild為其后繼;
(b) 否則,*p有右孩子 ——類似地,可知
p->rchild為其后繼;
? 否則,
p->rchild為其后繼線索;
由此得算法如下:
后序前驅的求解
分析:
(a) 若p有右孩子 —— 右孩子結點是其前驅;
(b) 否則,若P有左孩子 —— 左孩子結點是其前驅;
? 否則 —— p -> lchild是其前驅線索 。
與先序后繼的求解方式對稱。
由此得算法如下:
(6) 后序后繼的求解
分析:
(a) 根 —— 無后繼;
(b) 若*p是其父結點的右孩子 —— 父結點是其后繼;
? 若是父結點的左孩子 ——
無右兄弟 —— 父結點是其后繼;
有右兄弟
—— 右兄弟子樹的后序序列的第一個結點是其后繼
——右兄弟子樹中最左下的葉子結點。
線索二叉樹中插入結點
在結構中插入一個元素或結點是常見的運算。
在線索二叉樹中插入一個結點時,不僅要實現(xiàn)指定結點的父子關系的運算,還需要在插入結點后,通過修改線索,以維護二叉樹的線索關系。例如,在右圖線索二叉樹中,將某結點插入到作為結點F的左孩子。
如何實現(xiàn)相關的操作?
分析:對這類插入操作,通常從兩個方面來考慮其實現(xiàn):
(1)一個是父子關系的連接實現(xiàn)
按照指定關系連接即可
(2)線索關系的維護
這一關系的實現(xiàn)有一定的難度。
下面分別討論。
(1)父子關系的連接實現(xiàn)
對前述問題,由問題描述可知,
假設指針P指示到了F結點,
連接操作如圖所示,操作如下:
p->lchild=s;
p->ltag=0;
(2)線索關系的維護
這一操作主要由如下組成:
(某些操作可能因具體問題而不同)
設置新插入結點的前驅、后繼線索
修改新結點的前驅結點的后繼線索
修改新結點的后繼結點的前驅線索
例如,在右圖線索二叉樹中,
將某結點插入到作為結點F的左孩子。
如何實現(xiàn)相關的操作?
下面先討論線索操作的一般性方法,
然后給出本題的操作實現(xiàn)。假設當前結點為P,由于線索關系使得結點之間建立了線性關系,因此,插入結點時的線索維護類似于雙鏈表中插入結點。由于新結點是作為P的后繼,
因此,可有如下的邏輯圖:
. 二叉樹的線索化算法實現(xiàn)
——先序線索化為例
前驅結點的后繼線索化;
當前結點的前驅線索化;
判斷當前結點是否需要后繼線索化
遍歷線索二叉樹
//利用前面的尋找前驅和后繼節(jié)點的算法 中序 void inOrder() { if(root !=NULL) { ThreadBTNode<DataType>*s = root; while(prior(s)!=NULL) { s = prior(s);//找到起始結點 } while(succ(s)!=NULL) { cout<<s->data<<" "; s = succ(s);//從起始結點開始遍歷節(jié)點 } }18.分塊查找
基本思想:先插索引表,利用順序查找或者二分查找確定待查關鍵字k,屬于哪一個塊
根據(jù)索引表中塊首記錄的地址找到主表中塊的起始地址,利用順序查找的方法查找k,如果找到則返回地址,否則返回null
總結
以上是生活随笔為你收集整理的【练习】2021下半年数据结构刷题笔记和总结 (二) 树、查找-- 不同的排序算法、二叉排序树 平衡二叉树、哈希表查找、线索二叉树、的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【学习笔记】mongodb的使用(二)f
- 下一篇: 【flask学习笔记】flask与HTT