Java基礎資料結構和演算法
學習程式設計的第一天,就被告知程式=資料結構+ 演算法。作為一名開發者,雖然平時單獨研究資料結構和演算法的情況不多,但也一直在用。這些基礎知識和思想伴隨著自己寫的每一句程式碼。
資料結構
Array陣列
和C/C++以及其他語言一樣,Java中的陣列有差不多一樣的語法。只是Java
中除了8中基本型別,陣列也是作為物件處理的,所以建立物件時也需要使用new關鍵字。和大多數程式語言一樣,陣列一旦建立,大小便不可改變。
Java中有一個Arrays
類,專門用來操作array。
Arrays
中擁有一組static函式,
- equals()
:比較兩個array是否相等。array擁有相同元素個數,且所有對應元素兩兩相等。
- fill()
-
sort()
:用來對array進行排序。 -
binarySearch()
:在排好序的array中尋找元素。 -
System.arraycopy()
:array的複製。
int [] intArr = new int[10];
Array是Java中隨機訪問一連串物件最有效率的資料結構,但很不靈活,大小固定,且不知道里面有多少元素。為此JDK已經為我們提供了一系列相應的類來實現功能強大且更靈活的基本資料結構。這些類均在java.util
包中。其繼承結構如下:
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
│ └SortedSet
└Queue
-Map
├HashTable
├HashMap
└WeakHashMap
List
List
是一個介面,不能例項化,需要例項化一個ArrayList
或者LinkedList
。
ArrayList
裡面的內部實現,是通過一定的增長規則動態複製增加陣列長度來實現動態增加元素的。如果在大資料量的情況下,在某一個位置隨機插入或者刪除元素,就會產生效能問題。LinkedList
可以解決這類問題,但LinkedList
在通過下標取元素的時候,需要遍歷整個連結串列節點匹配,資料量大的情況下,效率不高。Vector
是一種老的動態陣列,是執行緒同步的,效率很低,一般不贊成使用。Stack
遍歷時刪除問題
使用增強for迴圈遍歷List
(所有實現子類ArrayList,Stack等)元素對其中元素進行刪除,會丟擲java.util.ConcurrentModificationException
的異常。若使用下面這種方式:
for(int i = 0;i < list.size();i++){
list.remove(i);
}
則會刪除下標為偶數的元素,因為每次刪除後,後面的元素的下標全部減1,相當於元素位置全部左移一位,再次刪除時,會跳過一個元素進行刪除。這是非常不好的。如果非得要這樣刪除,可以倒著來:
for(int i = list.size()-1 ;i >= 0 ;i--){
list.remove(i);
}
或者新建一個要刪除的List,最後一起刪除。list.removeAll(deleteList);
Set
Set
介面繼承Collection
介面,最大的特點是集合中的元素都是唯一的,沒有重複。它有兩個子類,HashSet
和TreeSet
。
HashSet
- 不允許出現重複元素;
- 不保證集合中元素的順序。雜湊演算法來的~
- 允許包含值為
null
的元素,但最多隻能有一個null
元素。
TreeSet
- 不允許出現重複元素;
- 集合中元素的順序按某種規則進行排序
- 不允許包含值為
null
的元素
Map
Map介面,沒有繼承Collection介面,它是獨立的一個介面。它使用key-value的鍵值對儲存資料。常用的兩個子類是HashMap和TreeMap。
- HashMap:Map
基於散列表的實現。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設定容量capacity和負載因子load factor,以調整容器的效能。
- LinkedHashMap
: 類似於HashMap,但是迭代遍歷它時,取得“鍵值對”的順序是其插入次序,或者是最近最少使用(LRU
)的次序。只比HashMap慢一點。而在迭代訪問時發而更快,因為它使用連結串列維護內部次序。
- TreeMap
: 基於紅黑樹資料結構的實現。檢視“鍵”或“鍵值對”時,它們會被排序(次序由Comparabel
或Comparator
決定)。TreeMap的特點在 於,你得到的結果是經過排序的。TreeMap是唯一的帶有subMap()
方法的Map,它可以返回一個子樹。
- WeakHashMao
:弱鍵(weak key)Map,Map中使用的物件也被允許釋放: 這是為解決特殊問題設計的。如果沒有map之外的引用指向某個“鍵”,則此“鍵”可以被垃圾收集器回收。
- IdentifyHashMap
: : 使用==代替equals()對“鍵”作比較的hash map。專為解決特殊問題而設計。
執行緒安全
Vector
是執行緒同步的,也就是執行緒安全的,對多執行緒的操作採用了synchronized
處理。但因為效率低,已不建議使用。ArrayList
和LinkedList
都是執行緒不安全的,在多執行緒環境中,對資料的修改會造成錯誤的結果。有兩種解決方案:
使用同步包裝器
List safedList = Collections.synchronizedList(new ArrayList());
Set safedSet=Collections.synchronizedSet(new HashSet());
Map safedMap=Collections.synchronizedMap(new HashMap());
檢視其原始碼,發現是Collections
類給不安全的集合類包裝了一層,然後生成一個新的類,新類裡面採用了synchronized
對集合的操作進行了同步處理。
...
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
...
使用安全的集合類
Java5.0
新加入的ConcurrentLinkedQueue
、ConcurrentHashMap
、CopyOnWriteArrayList
和CopyOnWriteArraySet
,這些集合類都是執行緒安全的。這些類在 java.util.concurrent
包下。
而至於這些新的類為什麼能保證執行緒安全,這裡不作詳述,可以參考網上大牛的分析。
Android中的List、Map替代方案
SimpleArrayMap
SparseArray與SparseArrayCompat和LongSparseArray
這3個類中,前2個基本上是同一類,只不過第二個類有removeAt方法,第三個是Long型別的。
這3個類也是用來代替HashMap,只不過他們的鍵(key)的型別是整型Integer或者Long型別,在實際開發中,如月份縮寫的對映,或者進行檔案快取對映,ViewHolder都特別適用
AtomicFile
AtomicFile首先不是用來代替File的,而是作為File的輔助類存在, AtomicFile的作用是實現事務性原子操作,即檔案讀寫必須完整,適合多執行緒中的檔案讀寫操作。
用來實現多執行緒中的檔案讀寫的安全操作
演算法
二分查詢
對於有序陣列
,二分查詢的效率在大資料量的情況下,效率明顯:
private static int find(int [] arr,int searchKey){
int lowerBound = 0;
int upperBound = arr.length -1;
int curIn;
while(lowerBound <= upperBound){
curIn = (lowerBound + upperBound) / 2;
if(arr[curIn] == searchKey){
return curIn;
}else{
if(arr[curIn] < searchKey){
lowerBound = curIn + 1;
}else{
upperBound = curIn - 1;
}
}
}
return -1;
}
使用遞迴的方式編寫,貌似看起來好理解點:
private static int recursiveFind(int[] arr,int start,int end,int searchKey){
if (start <= end) {
// 中間位置
int middle = (start + end) >> 1; // (start+end)/2
if (searchKey == arr[middle]) {
// 等於中值直接返回
return middle;
} else if (searchKey < arr[middle]) {
// 小於中值時在中值前面找
return recursiveFind(arr, start, middle - 1, searchKey);
} else {
// 大於中值在中值後面找
return recursiveFind(arr, middle + 1, end, searchKey);
}
} else {
// 找不到
return -1;
}
}
排序
簡單排序
氣泡排序
對亂序的陣列,很常見的排序方法是氣泡排序:
private static void bubbleSrot(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = i + 1; j < arr.length; j++) {
if(arr[i] > arr[j]){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
這種排序方法速度是很慢的,執行時間為O(N²)級。
選擇排序改進了氣泡排序,將必要的交換次數從O(N²)減少到O(N),不幸的是比較次數依然是O(N²)級。
然而,選擇排序依然為大記錄量的排序提出了一個非常重要的改進,因為這些大量的記錄需要在記憶體中移動,這就使交換的時間和比較的時間相比起來,交換的時間更為重要。(一般來說,Java語言中不是這種情況,Java中只是改變了引用位置,而實際物件的位置並沒有發生改變)
選擇排序
private static void chooseSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int least = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[least]) {
least = j;
}
}
// 將當前第一個元素與它後面序列中的最小的一個 元素交換,也就是將最小的元素放在最前端
int temp = arr[i];
arr[i] = arr[least];
arr[least] = temp;
}
}
選擇排序的效率:選擇排序和氣泡排序執行了相同次數的比較:N*(N-1)/2。對於10個數據項,需要45次比較,然而,10個數據項只需要少於10次的交換。對於100個數據項,需要4950次比較,但只進行不到100次交換。N值很大時,比較的次數是主要的,所以結論是選擇排序和冒泡哦排序一樣運行了O(N²)時間。但是,選擇排序無疑更快,因為它進行的交換少得多。
插入排序
插入排序,在一般情況下,比氣泡排序快一倍,比選擇排序快一點。
private static void insertionSort(int[] arr){
int in,out;
for(out = 1 ; out < arr.length ; out ++){
int temp = arr[out];
in = out;
while(in > 0 && arr[in-1] >= temp){
arr[in] = arr[in - 1];
--in;
}
arr[in] = temp;
}
}
在外層的for迴圈中,out變數從1開始,向右移動。它標記了未排序部分的最左端資料。而在內層的while迴圈中,in變數從out變數開始,向左移動,直到temp變數小於in所指的陣列資料項,或者它已經不能再向左移動為止。while迴圈的每一趟都向左移動了一個已排序的資料項。
插入排序的效率:這個演算法中,第一趟排序,最多比較一次,第二趟排序,最多比較兩次,以此類推,最後一趟最多比較N-1次,因此有1+2+3+…+N-1 = N*(N-1)/2。然而,因為在每一趟排序發現插入點之前,平均只有全體資料項的一半真的進行了比較,所以除以2最後是N*(N-1)/4。
對於隨機順序的資料,插入排序也需要O(N²)的時間級。當資料基本有序,插入排序幾乎只需要O(N)的時間,這對把一個基本有序的檔案進行排序是一個簡單而有效的方法。
對於逆序排列的資料,每次比較和移動都會執行,所以插入排序不比氣泡排序快。
歸併排序
歸併排序比簡單排序要有效的多,至少在速度上是這樣的。氣泡排序、選擇排序、插入排序要用O(N²)的時間,而歸併排序只需要O(N*logN)的時間。
歸併排序的一個缺點是它需要在儲存器中有另一個大小等於被排序的資料項數目的陣列。如果初始陣列幾乎佔滿整個儲存器,那麼歸併排序將不能工作。但是,如果有足夠的空間,歸併排序會是一個很好的選擇。
原理是合併兩個已排序的陣列到一個數組:
//將兩個已排序的數組合併到第三個陣列上。
private static void merge(int[] arrA, int[] arrB, int[] arrC) {
int aDex = 0, bDex = 0, cDex = 0;
int sizeA = arrA.length;
int sizeB = arrB.length;
// A陣列和B陣列都不為空
while (aDex < sizeA && bDex < sizeB) {
if (arrA[aDex] < arrB[bDex]) {
arrC[cDex++] = arrA[aDex++];
} else {
arrC[cDex++] = arrB[bDex++];
}
}
//A陣列不為空,B陣列為空
while (aDex < sizeA) {
arrC[cDex++] = arrA[aDex++];
}
//A陣列為空,B陣列不為空
while (bDex < sizeB) {
arrC[cDex++] = arrB[bDex++];
}
}
於是,詳細的完整實現如下:
static class DArray{
private int [] theArray;
public DArray(int[] theArray) {
this.theArray = theArray;
}
//執行歸併排序
public void mergeSort(){
//複製一份出來
int [] workSpace = new int [theArray.length];
reMergeSort(workSpace, 0, theArray.length-1);
}
private void reMergeSort(int [] workSpace,int lowerBound,int upperBound) {
if(lowerBound == upperBound){
return;
}else{
int mid = (lowerBound + upperBound) / 2;
reMergeSort(workSpace, lowerBound, mid);
reMergeSort(workSpace, mid + 1, upperBound);
merge(workSpace, lowerBound, mid + 1,upperBound);
}
}
private void merge(int [] workSpace,int lowPtr,int highPtr,int upperBound){
int j= 0;//workSpace's index
int lowerBound = lowPtr;
int mid = highPtr -1;
int n = upperBound - lowerBound + 1;
while(lowPtr <= mid && highPtr <= upperBound){
if(theArray[lowPtr] < theArray[highPtr]){
workSpace[j++] = theArray[lowPtr++];
}else{
workSpace[j++] = theArray[highPtr++];
}
}
while(lowPtr <= mid){
workSpace[j++] = theArray[lowPtr++];
}
while(highPtr <= upperBound){
workSpace[j++] = theArray[highPtr++];
}
for(j = 0;j < n ;j++){
theArray[lowerBound+j] = workSpace[j];
}
}
}
執行測試:
int b[] = new int[] { 3, 4, 1, 5, 5, 6, 8, 9, 7 };
DArray dArray = new DArray(b);
dArray.mergeSort();
System.out.println(Arrays.toString(b));//輸出結果:[1, 3, 4, 5, 5, 6, 7, 8, 9]
高階排序
有2個高階的排序演算法,希爾排序和快速排序。這兩種排序演算法都比簡單排序演算法快得多:希爾排序大約需要O(N*(logN)²)時間,快速排序需要O(N*logN)時間。這兩種排序演算法都和歸併排序不同,不需要大量的輔助儲存空間。希爾排序幾乎和歸併排序一樣容易實現,而快速排序是所有通用排序演算法中最快的一種排序演算法。
還有一種基數排序,是一種不常用但很有趣的排序演算法。
希爾排序
希爾排序是基於插入排序的。
private static void shellSort(int[] arr) {
int inner, outer;
int temp;
int h = 1;
int nElem = arr.length;
while (h <= nElem / 3) {
h = h * 3 + 1;
}
while (h > 0) {
for (outer = h; outer < nElem; outer++) {
temp = arr[outer];
inner = outer;
while (inner > h - 1 && arr[inner - h] >= temp) {
arr[inner] = arr[inner - h];
inner -= h;
}
arr[inner] = temp;
}
h = (h - 1) / 3;
}
}
快速排序
快速排序是最流行的排序演算法,在大多數情況下,快速排序都是最快的,執行時間是O(N*logN)級。
劃分
劃分是快速排序的根本機制。劃分本身也是一個有用的操作。
劃分資料就是把資料分為兩組,使所有關鍵字大於特定值的資料項在一組,所有關鍵字小於特定值的資料項在另一組。
private static int partitionIt(int[] arr ,int left,int right,int pivot){
int leftPtr = left - 1;
int rightPtr = right + 1;
while(true){
while(leftPtr < right && arr[++leftPtr] < pivot);
while(rightPtr > 0 && arr[--rightPtr] > pivot);
if(leftPtr >= rightPtr){
break;
}else{
//交換leftPtr和rightPtr位置的元素
int temp = arr[leftPtr];
arr[leftPtr] = arr[rightPtr];
arr[rightPtr] = temp;
}
}
return leftPtr;//返回樞紐位置
}
快速排序
//快速排序
private static void recQuickSort(int arr [] ,int left,int right){
if(right - left <= 0){
return;
}else{
int pivot = arr[right];//一般使用陣列最右邊的元素作為樞紐
int partition = partitionIt(arr, left, right, pivot);
recQuickSort(arr, left, partition-1);
recQuickSort(arr, partition+1, right);
}
}
//劃分
private static int partitionIt(int[] arr ,int left,int right,int pivot){
int leftPtr = left - 1;
//int rightPtr = right + 1;
int rightPtr = right ; //使用最右邊的元素作為樞紐,劃分時就要將最右端的資料項排除在外
while(true){
while(arr[++leftPtr] < pivot);
while(rightPtr > 0 && arr[--rightPtr] > pivot);
if(leftPtr >= rightPtr){
break;
}else{
//交換leftPtr和rightPtr位置的元素
int temp = arr[leftPtr];
arr[leftPtr] = arr[rightPtr];
arr[rightPtr] = temp;
}
}
//交換leftPtr和right位置的元素
int temp = arr[leftPtr];
arr[leftPtr] = arr[right];
arr[right] = temp;
return leftPtr;//返回樞紐位置
}
最後測試,10萬條隨機資料,排序完成耗時18~25ms。希爾排序耗時差不多,而簡單排序中的插入排序和選擇排序耗時3500ms以上,氣泡排序最慢,超過17000ms以上才完成;歸併排序比希爾排序和快速排序稍微慢點,在30ms左右。
文章是查找了很多的文章,還有Java資料結構和演算法.(第二版)這本書總結出來的。作為一個Java開發者,這些基礎的知識必須掌握,並且對其原理和原始碼要有所理解和領悟。感謝網上的大牛和無私的小夥伴們的分享。歡迎糾正和探討。