数据结构与算法基本思想
目錄
- 二分查找
- 八大排序
- 特點
- 冒泡排序
- 快速排序
- 直接插入排序
- 折半插入排序
- 希爾排序
- 簡單選擇排序
- 堆排序
- 二路歸并排序
- 基數排序
- 二叉查找樹(二叉排序樹)
- 哈希表(散列表)
- 1.概念
- 2.特點
- 3.怎么處理散列沖突?
- 4.散列表java代碼實現
- 拓撲排序
- 1.基本思想:
- 動態規劃
- 1.基本思想:
- 2.應用——將動態規劃拆分成三個子目標
- 分治法
- 基本概念
- 基本思想和策略
- 分治法的適用情況
- 分治法的基本步驟
- 依據分治法設計程序時的思維過程
- 貪心算法
- 基本概念
- 貪心算法的基本思路
- 貪心算法適用的問題
- 貪心算法的實現框架
- 貪心策略的選擇
- 字符串
- 紅黑樹
- 回溯法
- 概念
- 基本思想
- 解題的一般步驟
- 分支限界法
- 基本描述
- 分支限界法的一般過程
- 回溯法和分支限界法的一些區別
二分查找
java二分查找代碼
/*** 不使用遞歸的二分查找*title:commonBinarySearch*@param arr*@param key*@return 關鍵字位置*/ public static int commonBinarySearch(int[] arr,int key){int low = 0;int high = arr.length - 1;int middle = 0; //定義middleif(key < arr[low] || key > arr[high] || low > high){return -1; }while(low <= high){middle = (low + high) / 2;if(arr[middle] > key){//比關鍵字大則關鍵字在左區域high = middle - 1;}else if(arr[middle] < key){//比關鍵字小則關鍵字在右區域low = middle + 1;}else{return middle;}}return -1; //最后仍然沒有找到,則返回-1 }python二分查找代碼
def binary_search(lis, nun):left = 0right = len(lis) - 1while left <= right: #循環條件mid = (left + right) // 2 #獲取中間位置,數字的索引(序列前提是有序的)if num < lis[mid]: #如果查詢數字比中間數字小,那就去二分后的左邊找,right = mid - 1 #來到左邊后,需要將右變的邊界換為mid-1elif num > lis[mid]: #如果查詢數字比中間數字大,那么去二分后的右邊找left = mid + 1 #來到右邊后,需要將左邊的邊界換為mid+1else:return mid #如果查詢數字剛好為中間值,返回該值得索引return -1 #如果循環結束,左邊大于了右邊,代表沒有找到lis = [11, 32, 51, 21, 42, 9, 5, 6, 7, 8]print(lis)lis.sort()print(lis)while 1:num = int(input('輸入要查找的數:'))res = binary_search(lis, num)print(res)if res == -1:print('未找到!')else:print('找到!')八大排序
特點
①排序的平均時間復雜度?
②不穩定的排序有?
③排序的空間復雜度?
④其他特殊情況
①時間的復雜度:
o(log2n):快速排序,希爾排序,歸并排序,堆排序(快些歸隊),
特殊的為基數排序O(d(n+rd)),其他都是O(n2)
②不穩定的排序:快速排序,希爾排序,簡單選擇排序,堆排序(快些選對)
③空間復雜度:
歸并排序O(n),快速排序:o(log2n),基數排序:O(rd),其他都是O(1)
④其他特殊情況:
1.有序時:直接插入排序和冒泡排序時間復雜度為O(n),快排為O(n2)
冒泡排序
基本思想:從“第一位”開始,相鄰比較,把最值放到最后。如此循環
如果有 n 個數據,那么只需要比較 n–1 輪。而且除了第一輪之外,每輪都不用全部比較。因為經過前面輪次的比較
,已經比較過的輪次已經找到該輪次中最大的數并浮到右邊了,所以右邊的數不用比較也知道是大的。所以是n-1-i
java實現代碼:
public static int[] bubbleSortOnce(){int[] num = nums;for(int i=0; i<num.length ;i++){for(int j =1 ; j < num.length -i; j++){if(num[j] < num[j-1] ){int tmp = num[j];num[j] = num[j-1];num[j-1] = tmp; }}}python實現代碼:
def mp(sz):for i in range(0,len(sz)):for j in range(0,len(sz)-1):if sz[j]>sz[j+1]:temp=sz[j]sz[j]=sz[j+1]sz[j+1]=temp快速排序
基本思想:
- 從數列中挑出一個元素,稱為 “基準”;
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的后面(相同的數可以到任一邊)。在這個分區退出之后,該基準就處于數列的中間位置。這個稱為分區操作;
- 遞歸地把小于基準值元素的子數列和大于基準值元素的子數列排序。
java實現代碼:
void Function(int a[],int low,int high){int i=low,j=high,temp;if(low<high){temp=a[low];//把第一位作為基準while(i<j){while(i<j&&a[j]>=temp){j--;}if(i<j){a[i]=a[j];i++;}while(i<j&&temp>a[i]){i++;}if(i<j){a[j]=a[i];j--;}}a[i]=temp;Function(a,low,i-1);Function(a,i+1,high);} }python實現代碼:
def kp(a,low,high):i=lowj=highif low<high:temp=a[low]while i<j:while i<j and a[j]>=temp:j=j-1if i<j:a[i]=a[j]i=i+1while i<j and temp>a[i]:i= i+1if i<j:a[j]=a[i]j=j-1a[i]=tempkp(a,low,i-1)kp(a,i+1,high)直接插入排序
基本思想:每次從無序表中取出第一個元素,把它插入到有序表的合適位置,使有序表仍然有序
java實現代碼:
void Function(int a[],int n){int i,j;int temp;for(i=1;i<n;i++){temp=a[i];//從后往前比較,大于就往后移for(j=i-1;j>=0&&a[j]>temp;j--){a[j+1]=a[j];}//最后一位空位為j,但有j--,所以再+1a[j+1]=temp;} }python實現代碼:
def kp(a,n):for i in range(1,n):temp=a[i]j=i-1while j>=0 and a[j]>temp:a[j+1]=a[j]j=j-1a[j+1]=temp折半插入排序
思想:和直接插入排序類似,區別是查找方法不同,折半插入排序是二分查找法查找插入的位置。
java實現代碼:
希爾排序
基本思想:基與直接插入排序,遞減增量排序算法,是插入排序的一種更高效的改進版本.。
簡單選擇排序
基本思想:選出最小值,放在"第一位",然后"第一位"向后推移。如此循環。
java實現代碼:
void Function(int a[],int n){int temp,min;int i,j;for(i=0;i<n;i++){min=i;for(j=i+1;j<n;j++){if(a[j]<a[min]){min=j;}}temp=a[min];a[min]=a[i];a[i]=temp;}}python實現代碼:
def ks(sz):for i in range(0,len(sz)-1):min=i;for j in range(i+1,len(sz)): #注意:range(0,10)為0~9if sz[min]>sz[j]:min=j;temp = sz[i]sz[i] = sz[min]sz[min] = temp堆排序
基本思想:
-
將無需序列構層序遍歷順序建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
-
將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
-
重新調整結構,使其滿足堆定義,然后繼續交換堆頂元素與當前末尾元素,反復執行調整+交換步驟,直到整個序列有序。
任一個非葉節點的值都不大于其左右孩子的值,若父親大孩子小,則為大頂堆,若父親小孩子大,則為小頂堆。
原始序列對應二叉樹層序遍歷
調整步驟:
①從第一個非葉子結點開始,從右至左,從下至上(即按照層序序號遞減)調整。
②將當前結點的值與孩子結點比較,如果存在>a的孩子,則從中選出最大的一個和a交換。當a來到下一層時,重復上述操作,直至a的孩子結點值都小于a為止
例子:假設給定無序序列結構如下
①從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
找到第二個非葉節點4,由于[4,9,8]中9元素最大,4和9交換。
這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
此時,我們就將一個無需序列構造成了一個大頂堆。
②將堆頂元素與末尾元素進行交換,使末尾元素最大。然后繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反復進行交換、重建、交換。
將堆頂元素9和末尾元素4進行交換
重新調整結構,使其繼續滿足堆定義
再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
后續過程,繼續進行調整,交換,如此反復進行,最終使得整個序列有序
二路歸并排序
基本思想:歸并排序是建立在歸并操作上的一種有效的排序算法,該算法是采用分治法的一個非常典型的應用。將已有序的子序列合并,得到完全有序的序列;
即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合并成一個有序表,稱為二路歸并。
我們舉個例子,將兩個有序數組合并成一個有序數組,如下圖。
歸并排序的步驟,我們給定一個無序數組{9, 2, 6, 3, 5, 7, 10, 11}
不斷遞歸直至每個分組中都只有一個元素,即每個分組都是一個有序數組,不過只有一個元素一定是有序的。
java實現代碼:
ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType))//輔助數組Bvoid Merge(ElemType A[],int low,int mid,int high){for(int k=low;k<=high;k++){B[k]=A[k]; //將A中所有元素復制到B中}for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){if(B[i]<=B[j])A[k]=B[i++];elseA[k]=B[j++];}while(i<=mid) A[k++]=B[i++]; //若第一個表未檢測完,復制while(j<=high) A[k++]=B[j++]; //若第一個表未檢測完,復制 }void MergeSort(ElemType A[],int low,int high){if(low<high){int mid=(low+high)/2; //從中間劃分兩個子序列MergeSort(A,low,mid); //對左側子序列進行遞歸排序MergeSort(A,mid+1,high); //對右側子序列進行遞歸排序Merge(A,low,mid,high); //歸并} }python實現代碼:
def Merge(nums, low, mid, high):res = []i = lowj = mid+1while i<=mid and j<=high:if nums[i]<=nums[j]:res.append(nums[i])i += 1else:res.append(nums[j])j += 1while i<=mid:res.append(nums[i])i += 1while j<=high:res.append(nums[j])j += 1nums[low:high+1] = res[:] def MergeSort(nums, low, high):if low<high:mid = (low+high)/2MergeSort(nums, low, mid)MergeSort(nums, mid+1, high)Merge(nums, low, mid, high)基數排序
例子:
原始序列:278 109 063 930 589 184 505 269 008 083(過程)
有最高位優先,有最低位優先。
二叉查找樹(二叉排序樹)
1.概念:
(1)若左子樹不空,則左子樹上所有節點的值均小于它的根節點的值;
(2)若右子樹不空,則右子樹上所有節點的值均大于它的根節點的值;
(3)左、右子樹也分別為二叉排序樹(遞歸);
(4)沒有值相等的節點。
2.優點:數組的搜索比較方便,可以直接用下標,但刪除或者插入某些元素就比較麻煩;鏈表與之相反,刪除和插入元素很快,但查找很慢;二叉排序樹就既有鏈表的好處,也有數組的好處。
3.二叉排序樹的建立:
序列:50,38,30,45,40,48,70,60,75,80
4.二叉排序樹的查找,插入,刪除思路:
- 查找,插入:找到位置直接操作
- 刪除有三種情況:
1.被刪除結點P為葉子結點:直接刪除
2.被刪除結點P只有左子樹或只有右子樹:將被刪除結點P的左子樹或右子樹接成其雙親結點的左/右子樹
3.被刪除結點P有左子樹和右子樹:用被刪除結點P左子樹中最大的值(即最右端結點S)代替被刪除結點P,并釋放S(最右端結點S)
5.java代碼實現:
/**二叉排序樹中的查找算法*/public boolean searchBST(int key){Node current = root;while (current != null){//等于當前值,返回trueif(key == current.getValue())return true;//小于當前值,進入左節點else if (key < current.getValue())current = current.getLeft();//進入右節點elsecurrent = current.getRight();}//沒找到結果返回falsereturn false;}/**二叉排序樹中插入結點*/public boolean insertBST(int newKey){Node p = root;Node parent = null;//記錄插入結點位置(prev為要插入結點的父節點)Node prev = null;//在樹中尋找插入位置while (p != null){parent = p;prev = p;if(newKey > p.getValue())p = p.getRight();else if (newKey < p.getValue())p = p.getLeft();//鍵值為newKey的結點已在樹中,不再插入elsereturn false;}//若為空樹,鍵值為newKey的結點為樹根if (root == null)root = new Node(newKey,parent);//作為左結點插入else if (newKey < prev.getValue())prev.setLeft(new Node(newKey,parent));//作為右結點插入elseprev.setRight(new Node(newKey,parent));return true;}/**刪除結點,有三種情況:* 1.被刪除結點P為葉子結點:直接刪除* 2.被刪除結點P只有左子樹或只有右子樹:將被刪除結點P的左子樹或右子樹接成其雙親結點的左/右子樹* 3.被刪除結點P有左子樹和右子樹:* 用被刪除結點P左子樹中最大的值(即最右端結點S)代替被刪除結點P,并刪除左子樹中最大值(最右端結點S)*/public boolean deleteBST(int key){return deleteBST(root,key);}public boolean deleteBST(Node node,int key){if (node == null)return false;else {if (key == node.getValue())return deleteNode(node);else if (key < node.getValue())return deleteBST(node.getLeft(),key);elsereturn deleteBST(node.getRight(),key);}}private boolean deleteNode(Node node) {Node temp = null;//被刪除結點為葉子結點,直接刪除if(node.getLeft() == null && node.getRight() == null){//被刪除結點是根結點if(node == root)root = null;else{if(node.getValue() == node.getParent().getLeft().getValue())node.getParent().setLeft(null);elsenode.getParent().setRight(null);}}//被刪除結點P只有左子樹或只有右子樹,將被刪除結點P的左子樹或右子樹接成其雙親結點的左/右子樹else if (node.getLeft() == null){if(node.getValue() == node.getParent().getLeft().getValue())node.getParent().setLeft(node.getRight());elsenode.getParent().setRight(node.getRight());}else if (node.getRight() == null){if(node.getValue() == node.getParent().getLeft().getValue())node.getParent().setLeft(node.getLeft());elsenode.getParent().setRight(node.getLeft());}//被刪除結點P有左子樹和右子樹,//用被刪除結點P左子樹中最大的值(即最右端結點S)代替被刪除結點P,并刪除左子樹中最大值(最右端結點S)else {temp = node;Node s = node;/**在左子樹中獲取最右端結點S*/s = s.getLeft();while(s.getRight() != null){temp = s;s = s.getRight();}//S替代Pnode.setValue(s.getValue());//刪除Sif(temp != node){temp.setRight(s.getLeft());}else{temp.setLeft(s.getLeft());}}return true;}哈希表(散列表)
1.概念
散列表是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它通過計算一個關于鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
2.特點
- 優點:不論哈希表中有多少數據,查找、插入、刪除(有時包括刪除)只需要接近常量的時間即0(1)的時間級。
- 缺點:它是基于數組的,數組創建后難于擴展;一個關鍵字可能對應多個散列地址;需要查找一個范圍時,效果不好
3.怎么處理散列沖突?
散列沖突:不同的關鍵字經過散列函數的計算得到了相同的散列地址。
好的散列函數=計算簡單+分布均勻(計算得到的散列地址分布均勻)
① 開放定址法
一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,并將記錄存入。 發生沖突,另尋他處
我們把這種解決沖突的方法稱為線性探測法。 我們在解決沖突的時候,還會碰到比如說一個數 48, 它所在的位置已經被占用,它只能往后延,但是又與后面的沖突 ,本來兩個數一點關系都沒有,但是發生沖突,這種現象稱為堆積, 堆積的出現使得我們需要不斷處理沖突,即48要不斷向后延。存入以及查找的效率都會大大降低。
優化方案:改進 d i = 12 , -12 , 22, -22 …… q2 , -q2 (q <= m/2),這樣就等于可以是雙向尋找可能的位置了,
增加平方運算的目的是為了不讓關鍵字都聚集在某一個區域,我們稱這樣的方法為二次探測法。
改進方案: 在沖突時,對于位移量 d 1 采用隨機函數計算得到,我們稱之為隨機探測法。(偽隨機)
② 再散列函數法:
準備多個散列函數,比如前面說的除留余數、折疊、平方取中全部用上,每當發生散列地址沖突時,就換一個散列函數計算,這種方法使得關鍵字不產生聚焦,當然也會增加計算的時間 不斷嘗試。
③ 鏈地址法
將所有關鍵字為同義詞的記錄存儲在一個單鏈表中,我們稱這種表為同義詞子表,不是同義詞放在多個位置了,而是把他們集中起來, 在散列表里只存儲所有同義詞子表的頭指針。
鏈地址法對于可能會造成很多沖突的散列函數來說,提供了絕不會出現找不到地址的保障。當然,這也會增加查找時需要遍歷單鏈表的性能損耗。
4.散列表java代碼實現
節點數據結構
package com.billJiang.hashtable;/*** Created by billJiang on 2016/11/30.* hashtable 節點 可能發生碰撞*/ public class Entry<T> {int key;T item;Entry<T> next;public Entry(int key,T item,Entry<T> next){this.key=key;this.item=item;this.next=next;} }實現代碼
package com.billJiang.hashtable;import java.util.Arrays;/*** Created by billJiang on 2016/11/30.*/ public class HashTable<T> {private static final int INITIAL_SIZE=3;private static final float LOAD_FACTOR=0.75f;private Entry<T>[] table;private int size=0;private int use=0;public HashTable() {table = new Entry[INITIAL_SIZE];}public void put(int key,T item){int index=hash(key);if(table[index]==null){table[index]=new Entry(-1,null,null);}Entry e=table[index];//未存過值if(e.next==null){Entry entry=new Entry(key,item,null);e.next=entry;size++;use++;if(use>=table.length*LOAD_FACTOR){resize();}}else{//已經存在值,替換for(e=e.next;e!=null;e=e.next){if(e.key==key){e.item=item;return;}}//追加Entry temp=table[index].next;Entry entry=new Entry(key,item,temp);table[index].next=entry;size++;}}public void remove(int key){int index=hash(key);Entry e=table[index];Entry pre=table[index];for(e=e.next;e!=null;e=e.next){if(e.key==key){pre.next=e.next;size--;//TODO 24頁缺少以下兩行代碼if (pre.key == -1 && e.next == null)use--;break;}pre=e;}}public T get(int key){int index=hash(key);Entry e=table[index];for(e=e.next;e!=null;e=e.next){if(e.key==key){return (T) e.item;}}return null;}public int size(){return this.size;}public int getLength(){return table.length;}private int hash(int key){return key%table.length;}private void resize(){Entry[] oldTable=table;table=new Entry[table.length*2];use=0;for(int i=0;i<oldTable.length;i++){if(oldTable[i]!=null&&oldTable[i].next!=null){Entry e= oldTable[i];Entry next=e.next;while(e.next!=null){int index=hash(next.key);if(table[index]==null){table[index]=new Entry(-1,null,null);use++;}Entry temp=table[index].next;table[index].next=new Entry(next.key,next.item,temp);e=next;}}}} }拓撲排序
1.基本思想:
- 從有向圖中選擇一個沒有前驅(入度為0)的頂點輸出
- 刪除上面的頂點,并且刪除從該頂點發出的所有邊
- 重復上述兩步,直到剩余的圖中不存在沒有前驅的頂點為止
動態規劃
1.基本思想:
取決于該問題是否能用動態規劃解決的是這些”小問題“會不會被重復調用。
如果一個問題滿足以下兩點,能用動態規劃解決:
- 問題的答案依賴于問題的規模?,也就是問題的所有答案構成了一個數列。
- 大規模問題的答案可以由小規模問題的答案遞推得到,也就是? [公式] 的值可以由? [公式] 中的個別求得。
2.應用——將動態規劃拆分成三個子目標
- 建立狀態轉移方程:這一步是最難的,大部分人都被卡在這里。這一步沒太多的規律可說,只需抓住一個思維:當做已經知道? [公式] ~? [公式] 的值,然后想辦法利用它們求得 [公式] ?。
- 緩存并復用以往結果:這一步不難,但是很重要。如果沒有合適地處理,很有可能就是指數和線性時間復雜度的區別。
- 按順序從小往大算:這里的“小”和“大”對應的是問題的規模
分治法
基本概念
把一個復雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。這個技巧是很多高效算法的基礎,如排序算法(快速排序,歸并排序),傅立葉變換(快速傅立葉變換)……
任何一個可以用計算機求解的問題所需的計算時間都與其規模有關。問題的規模越小,越容易直接求解,解題所需的計算時間也越少。例如,對于n個元素的排序問題,當n=1時,不需任何計算。n=2時,只要作一次比較即可排好序。n=3時只要作3次比較即可,…。而當n較大時,問題就不那么容易處理了。要想直接解決一個規模較大的問題,有時是相當困難的。
基本思想和策略
分治法的設計思想是:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
分治策略是:對于一個規模為n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞歸地解這些子問題,然后將各子問題的解合并得到原問題的解。這種算法設計策略叫做分治法。
如果原問題可分割成k個子問題,1<k≤n,且這些子問題都可解并可利用這些子問題的解求出原問題的解,那么這種分治法就是可行的。由分治法產生的子問題往往是原問題的較小模式,這就為使用遞歸技術提供了方便。在這種情況下,反復應用分治手段,可以使子問題與原問題類型一致而其規模卻不斷縮小,最終使子問題縮小到很容易直接求出其解。這自然導致遞歸過程的產生。
分治與遞歸像一對孿生兄弟,經常同時應用在算法設計之中,并由此產生許多高效算法。
分治法的適用情況
分治法所能解決的問題一般具有以下幾個特征:
- 該問題的規模縮小到一定的程度就可以容易地解決
- 該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質。
- 利用該問題分解出的子問題的解可以合并為該問題的解;
- 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。
第一條特征是絕大多數問題都可以滿足的,因為問題的計算復雜性一般是隨著問題規模的增加而增加;
第二條特征是應用分治法的前提它也是大多數問題可以滿足的,此特征反映了遞歸思想的應用;、
第三條特征是關鍵,能否利用分治法完全取決于問題是否具有第三條特征,如果具備了第一條和第二條特征,而不具備第三條特征,則可以考慮用貪心法或動態規劃法。
第四條特征涉及到分治法的效率,如果各子問題是不獨立的則分治法要做許多不必要的工作,重復地解公共的子問題,此時雖然可用分治法,但一般用動態規劃法較好。
分治法的基本步驟
分治法在每一層遞歸上都有三個步驟:
- step1 分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題;
- step2 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題
- step3 合并:將各個子問題的解合并為原問題的解。
依據分治法設計程序時的思維過程
實際上就是類似于數學歸納法,找到解決本問題的求解方程公式,然后根據方程公式設計遞歸程序。
1、一定是先找到最小問題規模時的求解方法
2、然后考慮隨著問題規模增大時的求解方法
3、找到求解的遞歸函數式后(各種規模或因子),設計遞歸程序即可。
貪心算法
基本概念
所謂貪心算法是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的局部最優解。貪心算法沒有固定的算法框架,算法設計的關鍵是貪心策略的選擇。必須注意的是,貪心算法不是對所有問題都能得到整體最優解,選擇的貪心策略必須具備無后效性,即某個狀態以后的過程不會影響以前的狀態,只與當前狀態有關。所以對所采用的貪心策略一定要仔細分析其是否滿足無后效性。
貪心算法的基本思路
1.建立數學模型來描述問題。
2.把求解的問題分成若干個子問題。
3.對每一子問題求解,得到子問題的局部最優解。
4.把子問題的解局部最優解合成原來解問題的一個解。
貪心算法適用的問題
貪心策略適用的前提是:局部最優策略能導致產生全局最優解。實際上,貪心算法適用的情況很少。一般,對一個問題分析是否適用于貪心算法,可以先選擇該問題下的幾個實際數據進行分析,就可做出判斷。
貪心算法的實現框架
從問題的某一初始解出發;
while (能朝給定總目標前進一步)
{
利用可行的決策,求出可行解的一個解元素;
}
由所有解元素組合成問題的一個可行解;
貪心策略的選擇
因為用貪心算法只能通過解局部最優解的策略來達到全局最優解,因此,一定要注意判斷問題是否適合采用貪心算法策略,找到的解是否一定是問題的最優解。
字符串
紅黑樹
回溯法
概念
回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。
回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇并不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。
許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
基本思想
在包含問題的所有解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。
若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜索遍才結束。
而若使用回溯法求任一個解時,只要搜索到問題的一個解就可以結束。
解題的一般步驟
(1)針對所給問題,確定問題的解空間: 首先應明確定義問題的解空間,問題的解空間應至少包含問題的一個(最優)解。
(2)確定結點的擴展搜索規則
(3)以深度優先方式搜索解空間,并在搜索過程中用剪枝函數避免無效搜索。
分支限界法
基本描述
類似于回溯法,也是一種在問題的解空間樹T上搜索問題解的算法。但在一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出T中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函數值達到極大或極小的解,即在某種意義下的最優解。
(1)分支搜索算法
所謂“分支”就是采用廣度優先的策略,依次搜索E-結點的所有分支,也就是所有相鄰結點,拋棄不滿足約束條件的結點,其余結點加入活結點表。然后從表中選擇一個結點作為下一個E-結點,繼續搜索。
選擇下一個E-結點的方式不同,則會有幾種不同的分支搜索方式。
- FIFO搜索
- LIFO搜索
- 優先隊列式搜索
(2)分支限界搜索算法
分支限界法的一般過程
由于求解目標不同,導致分支限界法與回溯法在解空間樹T上的搜索方式也不相同。回溯法以深度優先的方式搜索解空間樹T,而分支限界法則以廣度優先或以最小耗費優先的方式搜索解空間樹T。
分支限界法的搜索策略是:在擴展結點處,先生成其所有的兒子結點(分支),然后再從當前的活結點表中選擇下一個擴展對點。為了有效地選擇下一擴展結點,以加速搜索的進程,在每一活結點處,計算一個函數值(限界),并根據這些已計算出的函數值,從當前活結點表中選擇一個最有利的結點作為擴展結點,使搜索朝著解空間樹上有最優解的分支推進,以便盡快地找出一個最優解。
分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜索問題的解空間樹。問題的解空間樹是表示問題解空間的一棵有序樹,常見的有子集樹和排列樹。在搜索問題的解空間樹時,分支限界法與回溯法對當前擴展結點所使用的擴展方式不同。在分支限界法中,每一個活結點只有一次機會成為擴展結點。活結點一旦成為擴展結點,就一次性產生其所有兒子結點。在這些兒子結點中,那些導致不可行解或導致非最優解的兒子結點被舍棄,其余兒子結點被加入活結點表中。此后,從活結點表中取下一結點成為當前擴展結點,并重復上述結點擴展過程。這個過程一直持續到找到所求的解或活結點表為空時為止。
回溯法和分支限界法的一些區別
有一些問題其實無論用回溯法還是分支限界法都可以得到很好的解決,但是另外一些則不然。也許我們需要具體一些的分析——到底何時使用分支限界而何時使用回溯呢?
回溯法和分支限界法的一些區別:
- 方法對解空間樹的搜索方式
- 存儲結點的常用數據結構
- 結點存儲特性常用應用
回溯法深度優先搜索堆棧活結點的所有可行子結點被遍歷后才被從棧中彈出找出滿足約束條件的所有解。
分支限界法廣度優先或最小消耗優先搜索隊列、優先隊列每個結點只有一次成為活結點的機會找出滿足約束條件的一個解或特定意義下的最優解。
總結
以上是生活随笔為你收集整理的数据结构与算法基本思想的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 中的短路逻辑是什么?
- 下一篇: java遗忘知识点