以數據結構與算法分析(Java語言描述)中2.26小題爲例進行解析
大小爲N的數組A,其主元素是一個出現超過N/2的元素(從而這樣的元素之多有一個)。例 {3,3,4,2,4,4,2,4,4}中有一個主元素4,而數組{3,3,4,2,4,4,2,4}沒有主元素
現在我們假設數組中一定有主元素,下面將主元素找出來
1.求已知有主元素的數組的主元素,利用快速排序算法將數組排序,找到其中間元素,複雜度爲O(NlogN)
int get(int A[], int n) {
quickSort(A);
return A[n/2];
}
public static void quickSort(int[] array){
if(array != null){
quickSort(array, 0, array.length-1);
}
}
private static void quickSort(int[] array,int beg,int end){
if(beg >= end || array == null)
return;
int p = partition(array, beg, end);
quickSort(array, beg, p-1);
quickSort(array, p+1, end);
}
快排最重要的是partition方法,利用low和high作爲遊標,low從前往後依次遞增,high從後往前依次遞減,下面有兩種思路。
一種是從兩邊同時開始並同時交換;
private static int partition(int[] array, int start, int end) {
int pivot = array[start];
int i = start, j = end;
while (i < j) {
//從數組頭尾兩端入手,直到遇到比pivot大(正向)/小(逆向)的元素
while (array[i] <= pivot && i <= end) { //思考:兩個內循環while的第二個判斷條件能不能改爲i<j?
i++;
}
while (array[j] > pivot && j >= start) {//思考:兩個內循環while的第二個判斷條件能不能改爲i<j?
j--;
}
if (i < j) { //交換array[i]與array[j]
array[i] = array[i] ^ array[j];
array[j] = array[i] ^ array[j];
array[i] = array[i] ^ array[j];
}
}
if (j != start) {//交換pivot與j索引下的元素
array[j] = array[start] ^ array[j];
array[start] = array[start] ^ array[j];
array[j] = array[start] ^ array[j];
}
return j;
}
外部while循環結束時,i和j的位置必然是j與i相鄰,且j在j的左邊這種情況,不管是第一個內部while循環使i越過j,還是第二個內部while循環使j越過i,此時i必定指向比pivot更大的元素,而j必定指向比pivot更小的元素,所以最後是將array[j]與pivot交換而不是array[i].那麼如果按思考的那樣將第二個判斷條件改爲i<j,那麼外部while循環結束的時候i與j必定指向同一個位置,但此時無法確定該位置的元素到底是比pivot小還是大,所以無法達到目的。
另一種partition是先從數組尾部開始遞減,遇到比軸點(pivot)小的,先賦值給array[low],再從左邊開始遞增,遇到比軸點(pivot)大的,賦值給array[high]
private static int partition1(int[] array, int start, int end) {
int pivot = 0;
//當開始位置小於結束位置時(數組有數據) 進行排序 也就是遞歸入口
if (start < end) {
//首先找到基準數 作爲比較的標準數 取數組開始位置 從哪裏開始 用哪個數當標準數 這個就是標準數
int stard = array[start];
//記錄需要進行排序的下標
int low = start;
int high = end;
//循環比對比標準數大和小的數字
while (low < high) {
//
while (low < high && stard <= array[high])
high--;
//如果標準數大於 右邊的數字,用低位數字替換右邊數字
array[low] = array[high];
//如果左邊的數字比標準數小
while (low < high && array[low] <= stard)
low++;
//如果左邊的數字比標準數大
//用左邊數字替換右邊數字
array[high] = array[low];
}
//把標準數賦給高或者低所在的元素
array[low] = stard;
pivot = low;
}
return pivot;
}
這種方法和上面的方法思路不一樣,而是先while再賦值,且在while條件中增加了low<high的條件,所以結束的標誌必定是low和high指向同一個位置,這個位置就是放置pivot的,爲什麼和第一個方法思考的情況不一樣呢?因爲在low與high相遇前,每次停止while循環後low與high指向的位置中的元素都已被賦值到另一邊(即當low追上high時,high能確保它指向的元素已經回到正確的位置,現在這個位置相當於“空的”,反之也是).究其原因,還是第二種方法的“粒度”比較小,它每完成一次while循環就賦值一次,而第一種方法的“粒度”較大,每完成兩次while循環才進行一次交換
2.利用主元素的特性求已知有主元素的數組的主元素,複雜度O(n)
思路是這樣的。我們規定一種規則,在數組{3,3,4,2,4,4,2,4,4}中,從前往後遍歷,當3遇上3時,就將3的角標記爲2,如:,
當再遇到4時,那麼其中一個3與4相抵消,重新變爲3,當再遇上2時,再次抵消,此時就從2元素重新計數,那最後剩下的元素即爲主元素
int get(int A[], int n) {
int result, cnt;
result = A[0]; cnt = 1;
for(int i=1; i<n; i++) {
if(A[i] == result) {
cnt++;
}
else if(cnt == 1) {
result = A[i];
cnt = 1;
} else {
cnt--;
}
}
return result;
}
3.利用主元素的特性求不知是否有主元素的數組的主元素,複雜度O(n)
在上個程序上進行加工,求出後判斷其數量是否佔總數50%,是就是主元素,否則無主元素,返回-1
int get(int A[], int n) {
int result, cnt;
result = A[0]; cnt = 1;
for(int i=1; i<n; i++) {
if(A[i] == result) {
cnt++;
}
else if(cnt == 1) {
result = A[i];
cnt = 1;
} else {
cnt--;
}
}
cnt=0;
for(int i=0;i<n;i++){
if(A[i]==result)
cnt++;
}
if(cnt>(n/2))
return result;
else
return -1;
}
3.大小爲N的數組,其主元素是一個出現超過N/2次的元素(一個數組可能沒有主元素)。爲求出數組的主元素(若沒有主元素時需要指出來)
遍歷數組中的每一個元素,並對各個元素的個數計數,遍歷過的元素可存入另一個數組,以減少計算量。最壞情形下,此算法需要計算N*(2+2*N)次,因此算法是O(n^2)的。這個算法簡單直觀,符合一般想法,缺點是時間複雜度較高。代碼如下:
public class demo1 {
public static void f1(ArrayList<Integer> list) {
int length=list.size();
int count;
//標識是否存在主元素。
boolean b=false;
//存儲已經計數過的元素,避免再次對相同元素計數。
ArrayList<Integer> list1=new ArrayList<>();
for(int i=0;i<length;i++) {
Integer a=list.get(i);
if(!list1.contains(a)) {
count=0; //記錄每個元素在數組中出現的次數,for循環之前重置
for(int j=0;j<length;j++) {
if(a==list.get(j))
count++;
}
//如果是主元素,打印出來並離開循環。
if(count>length/2) {
System.out.println("主元素是"+a);
b=true;
break;
}
//如果不是主元素,將這個元素存入數組中。
else
list1.add(a);
}
}
if(!b)
System.out.println("沒有主元素");
}
}
4.大小爲N的數組,其主元素是一個出現超過N/2次的元素(一個數組可能沒有主元素)
爲求出數組的主元素(若沒有主元素時需要指出來)根據主元素的特性,由原來的數組,生成新的長度爲原來一半的數組,使得如果一個元素是原來數組的主元素,它一定是新數組的主元素,迭代求解。新數組生成方法如下:將原來的數組元素兩兩配對(任意配對),如果兩個元素相同,就將這個元素存入新數組中,否則不存入,繼續比較下一組配對,直至比較完所有配對元素。如果原數組長度爲奇數,無法兩兩配對,可任意尋找一元素,進行時間複雜度爲O(n)的算法,確定此元素是否爲主元素,若是,直接得到主元素,程序結束;若否,刪去此元素,得到一個長度爲偶數的數組,且原來的主元素仍是主元素,利用此數組生成新的數組迭代。在下面的demo2類中,f1()的時間複雜度爲O(n),f2爲迭代,其時間複雜度遞推公式爲T(N)=2+2*N+T(N/2),所以f2()複雜度爲O(N+logN)=O(N),容易看出f3()的複雜度爲O(N),所以算法2的時間複雜度爲O(N)。證明和代碼如下:
下面證明一上述方法得到的新數組滿足要求,即原數組的主元素一定是新數組的主元素:
設原數組爲A,主元素是X。A的長度爲2n,所以X的個數pi(X)>=n+1;將A分成n個2元配對,根據鴿籠原理,至少有一個配對是(X,X)對,由生成新數組B的方法,此時B中加入一個X。在剩下的n-1個配對中,X的個數pi(X)>=n-1;若在剩下的配對中,沒有兩個元素相同的配對,此時B中只有一個元素X,X顯然是B的主元素;若在剩下的配對中,有元素相同的配對,不妨設爲(Y,Y),此時B中將加入一個Y,但在剩下的n-2個配對中,還存在至少n-1個X,根據鴿籠原理,這些配對中至少有一個(X,X)對,所以B中需要加入一個X,此時B中有兩個X,一個Y;剩下的配對個數爲n-3,X的個數pi(X)>=n-3。以此類推,在B中每加入一個非X的元素,必定要再加入一個X,再加上第一次加入的X,X在B中的個數一定大於B長度的一半,即X一定是B的主元素。
若A的長度是偶數,可先取A的任一個元素,遍歷A求出此元素的個數,若此元素是A的主元素,可立即得到結果,若此元素不是A的主元素,刪掉此元素的新A的主元素仍是原來的主元素。
public class demo2 {
//確定一個元素是否爲一個數組的主元素
public static boolean f1(ArrayList<Integer> list,Integer a) {
int count=0;
int size=list.size();
for(int i=0;i<size;i++)
if(list.get(i)==a)
count++;
if(count>size/2)
return true;
else
return false;
}
//通過迭代求出數組的可能主元素
public static Integer f2(ArrayList<Integer> list) {
int size=list.size();
//沒有主元素的情況
if(size==0)
return null;
//返回可能主元素
else if(size==1)
return list.get(0);
else {
//list長度爲奇數時,檢查最後一個元素是否是主元素
if(size%2!=0) {
if(f1(list,list.get(size-1)))
return list.get(size-1);
}
//比較所有配對,生成新數組list1,並進行迭代
ArrayList<Integer> list1=new ArrayList<>();
for(int i=0;i<(size/2)*2;i+=2) {
if(list.get(i)==list.get(i+1))
list1.add(list.get(i));
}
return f2(list1);
}
}
//計算一個數組的主元素
public static void f3(ArrayList<Integer> list) {
Integer a=f2(list);
if(a==null)
System.out.println("沒有主元素");
else if(f1(list,a))
System.out.println("主元素是"+a);
else
System.out.println("沒有主元素");
}
}