同一問題可用不同算法解決,而一個算法的質量優劣將影響到算法乃至程序的效率。算法分析的目的在於選擇合適算法和改進算法。
計算機科學中,算法的時間復雜度是一個函數,它定量描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號(Order)表述,不包括這個函數的低階項和首項系數。使用這種方式時,時間復雜度可被稱為是漸近的,它考察當輸入值大小趨近無窮時的情況。
定義
在計算機科學中,算法的時間復雜度是一個函數,它定量描述了該算法的運行時間。這是一個關於代表算法輸入值的字符串的長度的函數。時間復雜度常用大O符號表述,不包括這個函數的低階項和首項系數。
算法復雜度
算法復雜度分為時間復雜度和空間復雜度。其作用: 時間復雜度是指執行算法所需要的計算工作量;而空間復雜度是指執行這個算法所需要的內存空間。(算法的復雜性體現在運行該算法時的計算機所需資源的多少上,計算機資源最重要的是時間和空間(即寄存器)資源,因此復雜度分為時間和空間復雜度)。
時間復雜度
1. 一般情況下,算法的基本操作重復執行的次數是模塊n的某一個函數f(n),因此,算法的時間復雜度記做:T(n)=O(f(n))
分析:隨着模塊n的增大,算法執行的時間的增長率和 f(n) 的增長率成正比,所以 f(n) 越小,算法的時間復雜度越低,算法的效率越高。
2. 在計算時間復雜度的時候,先找出算法的基本操作,然后根據相應的各語句確定它的執行次數,再找出 T(n) 的同數量級(它的同數量級有以下:1,log(2)n,n,n log(2)n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n) = 該數量級,若 T(n)/f(n) 求極限可得到一常數c,則時間復雜度T(n) = O(f(n))
例:算法:
則有 T(n) = n 的平方+n的三次方,根據上面括號里的同數量級,我們可以確定 n的三次方 為T(n)的同數量級
則有 f(n) = n的三次方,然后根據 T(n)/f(n) 求極限可得到常數c
則該算法的時間復雜度:T(n) = O(n^3) 注:n^3即是n的3次方。
3.在pascal中比較容易理解,容易計算的方法是:看看有幾重for循環,只有一重則時間復雜度為O(n),二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查找,如果一個for循環套一個二分,那么時間復雜度則為O(nlogn)。
常用排序
名稱 |
復雜度 |
說明 |
備注 |
冒泡排序 |
O(N*N) |
將待排序的元素看作是豎着排列的“氣泡”,較小的元素比較輕,從而要往上浮 |
|
插入排序 Insertion sort |
O(N*N) |
逐一取出元素,在已經排序的元素序列中從后向前掃描,放到適當的位置 |
起初,已經排序的元素序列為空 |
選擇排序 |
O(N*N) |
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小元素,然后放到排序序列末尾。以此遞歸。 |
|
快速排序 Quick Sort |
O(n *log2(n)) |
先選擇中間值,然后把比它小的放在左邊,大的放在右邊(具體的實現是從兩邊找,找到一對后交換)。然后對兩邊分別使用這個過程(遞歸)。 |
|
堆排序HeapSort |
O(n *log2(n)) |
利用堆(heaps)這種數據結構來構造的一種排序算法。堆是一個近似完全二叉樹結構,並同時滿足堆屬性:即子節點的鍵值或索引總是小於(或者大於)它的父節點。 |
近似完全二叉樹 |
希爾排序 SHELL |
O(n1+£) 0<£<1 |
選擇一個步長(Step) ,然后按間隔為步長的單元進行排序.遞歸,步長逐漸變小,直至為1. |
|
箱排序 |
O(n) |
設置若干個箱子,把關鍵字等於 k 的記錄全都裝入到第k 個箱子里 ( 分配 ) ,然后按序號依次將各非空的箱子首尾連接起來 ( 收集 ) 。 |
分配排序的一種:通過" 分配 " 和 " 收集 " 過程來實現排序。 |
冒泡排序
冒泡排序(BubbleSort)的基本概念是:依次比較相鄰的兩個數,將小數放在前面,大數放在后面。即在第一趟:首先比較第1個和第2個數,將小數放前,大數放后。然后比較第2個數和第3個數,將小數放前,大數放后,如此繼續,直至比較最后兩個數,將小數放前,大數放后。
冒泡排序流程至此第一趟結束,將最大的數放到了最后。在第二趟:仍從第一對數開始比較(因為可能由於第2個數和第3個數的交換,使得第1個數不再小於第2個數),將小數放前,大數放后,一直比較到倒數第二個數(倒數第一的位置上已經是最大的),第二趟結束,在倒數第二的位置上得到一個新的最大數(其實在整個數列中是第二大的數)。如此下去,重復以上過程,直至最終完成排序。
由於在排序過程中總是小數往前放,大數往后放,相當於氣泡往上升,所以稱作冒泡排序。
編碼思路:
用二重循環實現,外循環變量設為i,內循環變量設為j。假如有10個數需要進行排序,則外循環重復9次,內循環依次重復9,8,...,1次。每次進行比較的兩個元素都是與內循環j有關的,它們可以分別用a[j]和a[j+1]標識,i的值依次為1,2,...,9,對於每一個i,j的值依次為1,2,...10-i。
/* * 冒泡排序 */ public class BubbleSort { public static void main(String[] args) { int[] arr={9,8,7,6,5,4,3,2,1}; System.out.println("排序前數組為:"); for(int num:arr){ System.out.print(num+" "); } for(int i=0;i<arr.length-1;i++){//外層循環控制排序趟數 for(int j=0;j<arr.length-1-i;j++){//內層循環控制每一趟排序多少次 if(arr[j]>arr[j+1]){ int temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp; } } } System.out.println(); System.out.println("排序后的數組為:"); for(int num:arr){ System.out.print(num+" "); } } }
插入排序
有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入后此數據序列仍然有序,這個時候就要用到一種新的排序方法--插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間復雜度為O(n^2)。是穩定的排序方法。
插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最后一個元素除外(讓數組多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成后,再將這個最后元素插入到已排好序的第一部分中。
1、將指針指向某個元素,假設該元素左側的元素全部有序,將該元素抽取出來,然后按照從右往左的順序分別與其左邊的元素比較,遇到比其大的元素便將元素右移,直到找到比該元素小的元素或者找到最左面發現其左側的元素都比它大,停止;
2、此時會出現一個空位,將該元素放入到空位中,此時該元素左側的元素都比它小,右側的元素都比它大;
3、指針向后移動一位,重復上述過程。每操作一輪,左側有序元素都增加一個,右側無序元素都減少一個。
編碼思路:
需要兩層循環,第一層循環index表示上述例子中的指針,即遍歷從坐標為1開始的每一個元素;第二層循環從leftindex=index-1開始,leftindex--向左遍歷,將每一個元素與i處的元素比較,直到j處的元素小於i出的元素或者leftindex<0;遍歷從i到j的每一個元素使其右移,最后將index處的元素放到leftindex處的空位處。
public class InsertSort { private int[] array; private int length; public InsertSort(int[] array){ this.array = array; this.length = array.length; } public void display(){ for(int a: array){ System.out.print(a+" "); } System.out.println(); } /** * 插入排序方法 */ public void doInsertSort(){ for(int index = 1; index<length; index++){//外層向右的index,即作為比較對象的數據的index int temp = array[index];//用作比較的數據 int leftindex = index-1; while(leftindex>=0 && array[leftindex]>temp){//當比到最左邊或者遇到比temp小的數據時,結束循環 array[leftindex+1] = array[leftindex]; leftindex--; } array[leftindex+1] = temp;//把temp放到空位上 } } public static void main(String[] args){ int[] array = {38,65,97,76,13,27,49}; InsertSort is = new InsertSort(array); System.out.println("排序前的數據為:"); is.display(); is.doInsertSort(); System.out.println("排序后的數據為:"); is.display(); } }
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。 選擇排序是不穩定的排序方法。
1、從第一個元素開始,分別與后面的元素向比較,找到最小的元素與第一個元素交換位置;
2、從第二個元素開始,分別與后面的元素相比較,找到剩余元素中最小的元素,與第二個元素交換;
3、重復上述步驟,直到所有的元素都排成由小到大為止。
編程思路:
需要兩次循環,第一層循環i表示每輪指針指向的位置,將最小值min初始化為第i個元素,第二層循環從j=i+1開始,分別與min比較,如果小於min,則更新min的值,內層循環結束后;交換min元素和第i個元素的位置。以此類推進行下一輪循環,直到i=length時停止循環。當i=length時,說明小的元素已經全部移到了左面,因此無需進行內層循環了。
package com.test.insertsort; /** * 選擇排序 * @author Administrator * */ public class ChooseSort { private int[] array; private int length; public ChooseSort(int[] array){ this.array = array; this.length = array.length; } /** * 打印數組中的所有元素 */ public void display(){ for(int i: array){ System.out.print(i+" "); } System.out.println(); } /** * 選擇排序算法 */ public void chooseSort(){ for(int i=0; i<length-1; i++){ int minIndex = i; for(int j=minIndex+1;j<length;j++){ if(array[j]<array[minIndex]){ minIndex = j; } } int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } } public static void main(String[] args){ int[] array={100,45,36,21,17,13,7}; ChooseSort cs = new ChooseSort(array); System.out.println("排序前的數據為:"); cs.display(); cs.chooseSort(); System.out.println("排序后的數據為:"); cs.display(); } }
快速排序
設要排序的數組是A[0]……A[N-1],首先任意選取一個數據(通常選用數組的第一個數)作為關鍵數據,然后將所有比它小的數都放到它前面,所有比它大的數都放到它后面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序算法,也就是說,多個相同的值的相對位置也許會在算法結束時產生變動
注:在待排序的文件中,若存在多個關鍵字相同的記錄,經過排序后這些具有相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具有相同關鍵字的記錄之間的相對次序發生改變,則稱這種排序方法是不穩定的。
要注意的是,排序算法的穩定性是針對所有輸入實例而言的。即在所有可能的輸入實例中,只要有一個實例使得算法不滿足穩定性要求,則該排序算法就是不穩定的。
排序演示
示例
下標
|
0
|
1
|
2
|
3
|
4
|
5
|
數據
|
6
|
2
|
7
|
3
|
8
|
9
|
下標
|
0
|
1
|
2
|
3 |
4
|
5
|
數據
|
3
|
2
|
7
|
6
|
8
|
9
|
下標
|
0
|
1
|
2
|
3
|
4
|
5
|
數據
|
3
|
2
|
6
|
7
|
8
|
9
|
下標
|
0
|
1
|
2
|
3
|
4
|
5
|
數據
|
3
|
2
|
6
|
7
|
8
|
9
|
package com.test.insertsort; /** * 划分、遞歸、快排 * @author bjh * */ public class QuickSort { /**待排序、划分數組*/ private int[] array; /**數組長度*/ private int length; public QuickSort(int[] array){ this.array = array; this.length = array.length; } /** * 打印元素 */ public void printArray(){ for(int i=0; i<length; i++){ System.out.print(array[i]+" "); } System.out.println(); } /** * 划分 * @return 划分的分界點 */ public int partition(int left, int right, int pivot){ //左指針的起點,left-1是由於在后面的循環中,每循環一次左指針都要右移, //這樣可以確保左指針從左邊第一個元素開始,不然是從第二個開始 int leftpoint = left-1; //右指針的起點,right+1是由於后面的循環中,每循環一次右指針都要左移, //這樣可以確保右指針從最右邊開始,不然是從倒數第二個開始 int rightpoint = right+1; while(true){ //找到左邊大於pivot的數據,或者走到了最右邊仍然沒有找到比pivot大的數據 while(leftpoint<right && array[++leftpoint]<pivot); //找到右邊小於pivot的數據,或者走到了最左邊仍然沒有找到比pivot小的數據 while(rightpoint>left && array[--rightpoint]>pivot); //左指針和右指針重疊或相交 if(leftpoint >= rightpoint){ break; }else{ //交換左邊大的和右邊小的數據 swap(leftpoint,rightpoint); } } //返回分界點,即右邊子數組中最左邊的點 return leftpoint; } /** * 交換數據 */ public void swap(int leftpoint,int rightpoint){ int temp = array[leftpoint]; array[leftpoint] = array[rightpoint]; array[rightpoint] = temp; } public static void main(String args[]){ int[] array = {99,78,26,17,82,36,9,81,22,100,30,20,17,85}; QuickSort qs = new QuickSort(array); System.out.println("划分前的數據為:"); qs.printArray(); int bound = qs.partition(0, array.length-1, 50); System.out.println("划分后的數據為:"); qs.printArray(); System.out.println("划分的分界點為:" + array[bound] + ",分界點的坐標為:" + bound); } }
二叉樹遍歷
樹的特征和定義
樹是一種重要的非線性 數據結構,直觀地看,它是 數據元素(在樹中稱為結點)按分支關系組織起來的結構,很象自然界中的樹那樣。 樹結構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構都可用樹形象表示。樹在計算機領域中也得到廣泛應用,如在編譯源程序時,可用樹表示源程序的語法結構。又如在 數據庫系統中,樹型結構也是信息的重要組織形式之一。一切具有層次關系的問題都可用樹來描述。
樹(Tree)是元素的集合。我們先以比較直觀的方式介紹樹。下面的數據結構是一個樹:
樹有多個節點(node),用以儲存元素。某些節點之間存在一定的關系,用連線表示,連線稱為邊(edge)。邊的上端節點稱為父節點,下端稱為子節點。樹像是一個不斷分叉的樹根。
每個節點可以有多個子節點(children),而該節點是相應子節點的父節點(parent)。比如說,3,5是6的子節點,6是3,5的父節點;1,8,7是3的子節點, 3是1,8,7的父節點。樹有一個沒有父節點的節點,稱為根節點(root),如圖中的6。沒有子節點的節點稱為葉節點(leaf),比如圖中的1,8,9,5節點。從圖中還可以看到,上面的樹總共有4個層次,6位於第一層,9位於第四層。樹中節點的最大層次被稱為深度。也就是說,該樹的深度(depth)為4。
如果我們從節點3開始向下看,而忽略其它部分。那么我們看到的是一個以節點3為根節點的樹:
三角形代表一棵樹
再進一步,如果我們定義孤立的一個節點也是一棵樹的話,原來的樹就可以表示為根節點和子樹(subtree)的關系:
上述觀察實際上給了我們一種嚴格的定義樹的方法:
1. 樹是元素的集合。
2. 該集合可以為空。這時樹中沒有元素,我們稱樹為空樹 (empty tree)。
3. 如果該集合不為空,那么該集合有一個根節點,以及0個或者多個子樹。根節點與它的子樹的根節點用一個邊(edge)相連。
上面的第三點是以遞歸的方式來定義樹,也就是在定義樹的過程中使用了樹自身(子樹)。由於樹的遞歸特征,許多樹相關的操作也可以方便的使用遞歸實現。我們將在后面看到。
樹的實現
樹的示意圖已經給出了樹的一種內存實現方式: 每個節點儲存元素和多個指向子節點的指針。然而,子節點數目是不確定的。一個父節點可能有大量的子節點,而另一個父節點可能只有一個子節點,而樹的增刪節點操作會讓子節點的數目發生進一步的變化。這種不確定性就可能帶來大量的內存相關操作,並且容易造成內存的浪費。
一種經典的實現方式如下:
樹的內存實現
擁有同一父節點的兩個節點互為兄弟節點(sibling)。上圖的實現方式中,每個節點包含有一個指針指向第一個子節點,並有另一個指針指向它的下一個兄弟節點。這樣,我們就可以用統一的、確定的結構來表示每個節點。
計算機的文件系統是樹的結構,比如Linux文件管理背景知識中所介紹的。在UNIX的文件系統中,每個文件(文件夾同樣是一種文件),都可以看做是一個節點。非文件夾的文件被儲存在葉節點。文件夾中有指向父節點和子節點的指針(在UNIX中,文件夾還包含一個指向自身的指針,這與我們上面見到的樹有所區別)。在git中,也有類似的樹狀結構,用以表達整個文件系統的版本變化 (參考版本管理三國志)。
二叉樹:
二叉樹是由n(n≥0)個結點組成的有限集合、每個結點最多有兩個子樹的有序樹。它或者是空集,或者是由一個根和稱為左、右子樹的兩個不相交的二叉樹組成。
特點:
(1)二叉樹是有序樹,即使只有一個子樹,也必須區分左、右子樹;
(2)二叉樹的每個結點的度不能大於2,只能取0、1、2三者之一;
(3)二叉樹中所有結點的形態有5種:空結點、無左右子樹的結點、只有左子樹的結點、只有右子樹的結點和具有左右子樹的結點。
二叉樹(binary)是一種特殊的樹。二叉樹的每個節點最多只能有2個子節點:
二叉樹
由於二叉樹的子節點數目確定,所以可以直接采用上圖方式在內存中實現。每個節點有一個左子節點(left children)和右子節點(right children)。左子節點是左子樹的根節點,右子節點是右子樹的根節點。
如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜索樹(binary search tree)的特殊二叉樹。二叉搜索樹要求:每個節點都不比它左子樹的任意元素小,而且不比它的右子樹的任意元素大。
(如果我們假設樹中沒有重復的元素,那么上述要求可以寫成:每個節點比它左子樹的任意節點大,而且比它右子樹的任意節點小)
二叉搜索樹,注意樹中元素的大小
二叉搜索樹可以方便的實現搜索算法。在搜索元素x的時候,我們可以將x和根節點比較:
1. 如果x等於根節點,那么找到x,停止搜索 (終止條件)
2. 如果x小於根節點,那么搜索左子樹
3. 如果x大於根節點,那么搜索右子樹
二叉搜索樹所需要進行的操作次數最多與樹的深度相等。n個節點的二叉搜索樹的深度最多為n,最少為log(n)。
二叉樹的遍歷
遍歷即將樹的所有結點訪問且僅訪問一次。按照根節點位置的不同分為前序遍歷,中序遍歷,后序遍歷。
前序遍歷:根節點->左子樹->右子樹
中序遍歷:左子樹->根節點->右子樹
后序遍歷:左子樹->右子樹->根節點
例如:求下面樹的三種遍歷
前序遍歷:abdefgc
中序遍歷:debgfac
后序遍歷:edgfbca