1. 程式人生 > >《Java數據結構和算法》- 數組

《Java數據結構和算法》- 數組

one set 初始化列表 移動 嘗試 復用 示例 尋找 失敗

Q: 數組的創建?

A: Java中有兩種數據類型,基本類型和對象類型,在許多編程語言中(甚至面向對象語言C++),數組也是基本類型。但在Java中把數組當做對象來看。因此在創建數組時,必須使用new操作符:

int [] objArray = null;     // defines a reference to an array
objArray = new int[100];    // creates the array, and sets objArray to refer to it

或使用等價的單語句聲明和定義:

int[] objArray = new int[100];

[]操作符對於編譯器來說是一個標誌,它說明正在命名的是數組對象而不是普通的變量。 由於數組是一個對象,所以它的名字(如本例中的objArray)是數組的一個引用,它不是數組本身。數組存儲在堆空間的某一個地址中,而objArray僅僅保存著這個地址。
數組有一個length屬性,通過它可以得知當前數組的大小。
正如大多數編程語言一樣,一旦創建數組,數組大小不可改變。

Q: 數組的初始化列表?

A: 創建一個對象數組如下:

AutoData[] cars = new AutoData[400];

除非將特定的值賦給數組的數據項,否則它們一直是特殊的null對象。如果嘗試訪問一個含有null的數組數據項,程序會出現Null Pointer Assignment的運行時錯誤,這主要是為了保證在讀取某個數據項之前要先對其賦值。

使用下面的語法可以對一個基本類型的數組初始化:

int[] nArray = {0, 3, 6, 9, 12, 15, 18, 21, 24, 27};

上面的語句可能簡單得令人驚訝,它同時取代了引用聲明和使用new來創建數組。在大括號中的數據被稱為初始化列表。數組大小由列表數據項的個數決定。

Q: 面向過程編程的數組示例?

A: 先介紹一個老式的面向過程編程(POP)的版本,然後再介紹一個可以達到同樣效果的面向對象編程(OOP)的版本。
關於POP和OOP的思想,可以參考博客《編程思想的理解(POP,OOP,SOA,AOP)》

示例:ArrayTest.java

A:

通常數據結構中存儲的數據項包含有好幾個字段,所以應該由對象而不是簡單類型來代表它們。在本示例中,為了簡化所以數組中存儲的數據類型為long型。

A: 在查找過程中,用searchKey一個一個地與數組中的數據項比較,如果循環變量i變化到最後一個數據項,但是仍舊沒有匹配上,這個值就不在數組中。

A: 在刪除過程中,找到刪除的數據項後,向前移動所有下標比它大的數據項來填補刪除後留下來的“坑”,並將number減一。

Q: 面向對象編程的數組示例(一)?

A: 上面示例是一個老式的面向過程的程序,那麽怎麽把它改成面向對象編程的程序呢?首先要將數據存儲(數組)從程序分離出來,程序的其他部分成為使用這個結構的用戶;其次是改進存儲結構和用戶之間的通信。

A: 現在把這個數組封裝在一個類中,叫做LowArray。可以將LowArray類想成是工具,LowAppTest類時工具的使用者,現在程序被劃分成兩個不同角色的類,這對於編寫一個面向對象的程序來說是關鍵的第一步。

示例:LowArray.java, LowArrayTest.java

A: 用來存儲數據對象的類有時被稱作容器類(container class),例如LowArray類,通常容器類不僅存儲數據,並且提供訪問數據的方法和其他諸如排序等復雜的操作。

Q: 面向對象編程的數組示例(二)?

A: 上面的示例只是演示了如何將一個程序劃分為類,但它並沒有帶給我們太多的實際價值,下面要做的是重新分配類之間的責任,從而可以獲得更多的OOP的好處。

A: 上面的示例中數據存儲結構的使用者必須知道數組的下標,setElement()和getElement()方法還是在低層次構思了,它們與普通Java數組中[]操作符沒什麽區別。但有些用戶只需要隨機訪問數組數據項,不太需要知道數組的下標,因此他不會意識到數組下標是有用或有關系的。

示例:HighArray.java, HighArrayTest.java

A: HighArray程序給出了數據結構的一個改進接口,類用戶(HighArrayTest)使用接口就不再考慮下標了,它取消了setElement()和getElement()方法,取而代之是insert(), find()和delete()。因為由類來負責處理下標問題,所以這些方法不再需要將下標當做參數。類用戶可以集中精力於在做什麽而不是怎麽做:什麽要被插入、刪除和訪問,而不是如何執行這些操作。

A: 請留意main()是多麽地短小簡單。在LowArray中必須由main()處理的細節現在被HighArray類中的方法解決了。比如insert()方法向數組下一個空位置上放置一個新的數據項,一個名為mSize的字段跟蹤記錄著數組中實際已有的數據項個數。main()方法不再需要為數組中還有多少數據項而擔心了。

A: 令人驚訝的是,類用戶甚至不必知道HighArray類中使用何種數據結構存儲數據,結構被隱藏在接口之後。

Q: 有序數組的Java示例 ?

A: 假設一個數組內的數據項都按關鍵字進行排序,這種數組被稱為有序數組?為什麽要進行排序呢?好處之一是可以通過二分查找顯著地提高查找速度。

A: 下面討論一下有序數組的Java代碼,OrderArray類的核心是find()方法,通過它可以二分查找來定位一個特定的數據項。

示例: AscOrderArray.java, OrderedArrayTest.java

find()方法通過將數組數據項範圍不斷對半分割來查找特定的數據項,這個方法在一開始設變量nL和nR指向數組的最左邊和最右邊非空的數據項。然後在while循環中,當前的下標nC被設置為這個範圍的中間值。

如果幸運的話,nC可能直接就指向所需的數據項,所以應先查看是否相等,如果是,則意味著找到該數據項,下一步就是返回它的下標nC。

循環中的每一步將範圍縮小一半,最終這個範圍會小到無法分割(當nL等於nR,範圍是一個數據項,所以還需要再一次循環),當範圍不再有效時停止查找,但沒有找到所需的數據項,所以返回下標-1,表示失敗。

技術分享圖片

Q: 有序數組的優缺點 ?

A: 優點:查找的速度比無序數組快多了。

缺點:插入操作中由於所有靠後的數據項都需要移動以騰開空間,所以速度慢。

有序數組和無序數組中的刪除操作都很慢,這是因為數據項必須向前移動來填補已刪除數據項的“坑”。

Q: 存儲對象的數組示例?

A: 上面的Java示例中,數據結構中只存儲long類型的簡單變量,存儲這些變量簡化了程序,但它對如何在現實世界中使用數據存儲結構來說沒有代表性,通常我們存儲的數據記錄是許多字段的結合。例如一條職員記錄需要存儲姓、名、年齡、社會保險號等等。

下面的Java示例中給出對象是如何存儲的,而不再是簡單類型的變量了。這裏使用泛型技術,能夠更高復用代碼。

示例: ObjectArray.java, Person.java, ObjectArrayTest.java

Q: 大O表示法?

A: 在比較算法時似乎應該說一些類似“算法A比算法B快兩倍”之類的話,但實際上這類陳述並沒有多大的意義。為什麽呢?這是由於當數據項個數變化時,對應的比例也會發生根本改變。有可能數據項增加了50%,則A比B快了3倍;或有可能只有一半的數據項,但現在A和B的速度是相同的。

A: 我們需要的是一個可以描述算法的速度是如何與數據項的個數相關聯的比較,這就用到大O表示法。

Q: 無序數組的插入時間?

A: 無論數組中的數據項個數N有多大,一次插入總是用相同的時間,新數據項總是被放在下一個有空的地方,即array[size++]。我們可以說向一個無序數組中插入一個數據項的時間T是一個常數K:

T = K

A: 在現實情況中,插入所需的實際時間(不管是納秒、微妙好還是其他單位)與以下因素有關:微處理器,編譯後可執行文件的執行效率等等。上面等式中的常數K包含了所有這些因素,在現實情況中要得到K的值,需要測量一次插入所花費的時間。K就等於這個時間。

Q: 數組線性查找的時間?

A: 在數組數據項的線性查找中,尋找特定數據項所需的比較次數平均為數據項總數的一半。即T = K * (N / 2)。K是上面所說的常數,將2並入K可以得到一個更方便的公式。新K‘值等於原先K除以2。新公式為:

T = K‘ * N

這個方程說明平均線性查找時間與數組的大小成正比。

Q: 數組二分查找時間?

A: 二分查找的公式如下:

T = K * log2(N)

時間T與以2為底N的對數成正比。

為什麽是以2為底的對數呢?
我們小時候經常玩猜數字遊戲,假設這個數字的範圍是1 - 100。我們習慣性地先從100的一半開始,然後不斷地再以一半的範圍去猜測。這個過程就是不斷地除以2,直到找到我們猜對的數字。反過來逆推這個過程,就是不斷乘以2,即多少次2的自乘,2x = 100。自然這個猜測次數x就是以2為底100的對數。

從底數為2轉換為底數為10需乘以3.322,我們可以將這個3.322常數也並入K‘,由此不必指定底數:

T = K‘ * lg(N)

關於這個3.322怎麽來的,請看下面的公式:
技術分享圖片

Q: 什麽是冪?

A: 冪,指乘方運算的結果。nm指將n自乘m次, 把nm看作乘方的結果,叫做“n的m次冪”。
其中,n稱為“底數”,m稱為“指數”。

A: 掌握以下運算規則:
1) 同底數冪相乘,底數不變,指數相加;
2) 同底數冪相除,底數不變,指數相減;
3) 冪的乘方,底數不變,指數相乘

A: 零指數冪
技術分享圖片

A: 負指數冪
技術分享圖片

A: 分數指數冪

技術分享圖片

Q: 什麽是指數函數?

A: 指數函數(exponential function),一般地,y=ax函數(a為常數且以a>0,a≠1)叫做指數函數,函數的定義域是R。

A: 基本性質:
1) 指數函數的定義域為R,這裏的前提是a大於0且不等於1。對於a不大於0的情況,則必然使得函數的定義域不連續,因此我們不予考慮,同時a等於0函數無意義一般也不考慮;
2) 指數函數的值域為(0,+∞);
3) a > 1時,則指數函數單調遞增;若0 < a < 1,則為單調遞減的;
4) 函數總是通過(0,1)這點;
5) 指數函數具有反函數,其反函數是對數函數;

A: 運算法則:
技術分享圖片

Q: 什麽是對數函數?

A: 如果ax=N(a > 0,且a≠1),那麽數x叫做以a為底N的對數,記作x=logaN,讀作以a為底N的對數。“log”是拉丁文logarithm(對數)的縮寫。

A: 一般地,函數y=logax(a>0,且a≠1)叫做對數函數,也就是說以冪為自變量,指數為因變量,底數為常量的函數,叫對數函數。

A: 通常我們將以10為底的對數叫常用對數(common logarithm),並把log10N記為lgN。

A: 在科學計數中常使用以無理數e=2.71828···為底數的對數,以e為底的對數稱為自然對數(natural logarithm),並且把logeN 記為InN。

A: 基本性質:
1) 定義域是{x 丨x > 0}, 值域為實數集R;
2) 對數函數的函數圖像恒過定點(1,0);
3) a>1時,在定義域上為單調增函數, 0 < a < 1時,在定義域上為單調減函數;

A: 運算法則:
技術分享圖片

Q: 幾個常用算法的大O比較?

A: 當比較算法時,並不在乎具體的微處理器芯片或編譯器,真正需要比較的是對應不同的N值,因此不需要常數。

A: 大O表示法使用大寫字母O,可以認為其含義是"order of"大約是的意思。
下標是總結我們目前為止討論過的算法的運行時間。
技術分享圖片

A: 通過它我們可以比較不同的大O值:O(1)是優秀,O(logN)是良好,O(N)是還可以,O(N2)則差一些。
大O表示法的實質並不是對運行時間給出實際值,而是表達了運行時間是如何受數據項個數所影響的。
技術分享圖片

Q: 為什麽不用數組表示一切?

A: 僅用數組似乎就可以完成所有工作,那為什麽不用它們來進行所有數據的存儲呢?
我們已經看到了許多關於數組的缺點。如在一個無序數組的插入時間很快,但是查找卻要花費較慢的時間。在一個有序數組中可以查找得很快,但插入卻花費很長的時間。對於這兩種數組而言,刪除操作時間也很慢。

如果有一種數據結構進行任何操作如插入、刪除和查找都很快(理想是O(1)或者是O(logN))的時間,那就好了。

數組的另一個問題便是它們被new創建後,大小尺寸就被固定了。但通常在開始設計程序時並不知道會有多少數據項會被放入數組中,所以需要猜它的大小。如果猜的數過大,會使數組中的某些元素永遠不會被填充而浪費空間。如果猜小了,會發生數組溢出。

A: Java中有一個被稱為Vector類,使用起來很像數組,但是它可以擴展,這些附加的功能是以效率作為代價的。

你可能想嘗試創建自己的向量(vector)類。當類用戶使用類中內部數組將要溢出時,插入算法創建一個大一點的數組,把舊數組中的內容復制到新數組中,然後再插入新數據項。整個過程對於類用戶來說是不可見的。

Q: 參考

  1. 《Java數據結構和算法》Robert Lafore 著,第2章 - 數組
  2. 代數中的冪
  3. 指數函數
  4. 對數函數

《Java數據結構和算法》- 數組