Java 排序之快速排序

以數據結構與算法分析(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,如: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("沒有主元素");
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章