演算法知識梳理(5) 陣列第二部分

澤毛發表於2017-12-21

一、概要

本文介紹了有關陣列的演算法第二部分的Java程式碼實現,所有程式碼均可通過 線上編譯器 直接執行,演算法目錄:

  • 找到最小的k個數
  • 連續子陣列的最大和
  • 連續子陣列的最大和(二維)
  • 求陣列當中的逆序對

二、程式碼實現

2.1 找到最小的 k 個數

問題描述

即尋找一列數中最小的k個數

解決思路

利用最大堆的特點,加入我們對一個長度為N的陣列p的前k個元素進行建堆操作,那麼p[0]p[0,..,k-1]的最大值,之後再對p[k,..,N-1]依次遍歷:

  • 如果p[i]小於p[0],那麼就說明它屬於p[0,..,i]最小的K個元素之一,而p[0]則一定不屬於p[0,..,N-1]的最小的k個元素,此時將p[i]p[0]交換,重新對[0,..,N-1]的部分進行建堆操作
  • 如果p[i]大於等於p[0],那麼就說明p[i]一定不屬於p中最小的k個元素,因此忽略

直到i遍歷到N-1為止,此時p[0,..,k-1]就是陣列p最小的K個元素。

程式碼實現

class Untitled {
	
	static void maxHeapify(int p[], int di, int length){
    	int li = (di<<1)+1;
    	int t = p[di];
    	while (li < length) {
        	if (li+1 < length && p[li+1] > p[li])
            	li++;
        	if (p[di] >= p[li])
            	break;
        	p[di] = p[li];
        	di = li;
        	li = (di<<1)+1;
    	}
    	p[di] = t;
	}

	static void buildMaxHeap(int p[], int length){
    	for(int i=(length>>1)-1; i >= 0; i--){
        	maxHeapify(p, i, length);
    	}
	}
	
	static void minKthNum(int p[] ,int k, int length) {
    	buildMaxHeap(p,k);
    	int t;
    	for (int i=k; i < length; i++) {
        	if (p[i] < p[0]) {
           		t = p[0]; p[0] = p[i]; p[i] = t;
				maxHeapify(p, 0, k);
			}
        }
    }
		
	public static void main(String[] args) {
		int p[] = new int[]{2, 3, 10, 2, 5, 6, 7, 20, 1, -5};
		minKthNum(p, 3, p.length);
		for (int i=0; i < 3; i++) {
			System.out.println(String.valueOf(p[i]));
		}
	}
}
複製程式碼

執行結果

>> 2
>> 1
>> -5
複製程式碼

2.2 連續字陣列的最大和

問題描述

陣列中的元素有正有負,在該陣列中找出一個連續子陣列,要求該連續子陣列中各元素的和最大,這個連續子陣列便被稱作最大連續子陣列。比如陣列{2,4,-7,5,2,-1,2,-4,3}的最大連續子陣列為{5,2,-1,2},最大連續子陣列的和為5+2-1+2=8

解決思路

通過對陣列中的元素進行線性的遍歷,並對每個元素進行累加,當發現目前為止累加的和maxendinghere小於0時,則說明最大的連續子陣列不可能包含目前遍歷到的子陣列,那麼就從下一個元素tmaxbegin開始計運算元陣列。

在遍歷的過程中更新目前位置獲得的最大連續子陣列的和maxsofar,以及起止位置maxbeginmaxend

程式碼實現

class Untitled {

	static void maxSumSubArray(int p[], int length){
		int maxendinghere = 0;
		int maxsofar = 0;
		int maxbegin = 0;
		int maxend = 0;
		int tmaxbegin = 0;
		//從0開始遍歷陣列。
		for(int i = 0; i < length; i++){
			//maxendinghere 記錄當前計運算元陣列的和。
			maxendinghere += p[i];
			//如果該和小於0,那麼重新計算。
			if(maxendinghere < 0){
				maxendinghere = 0;
				tmaxbegin = i+1;
			}
			//更新目前為止計算到的最大值。
			if(maxsofar < maxendinghere){
				maxbegin = tmaxbegin;
				maxend = i;
				maxsofar = maxendinghere;
			}
		}
		//考慮陣列全部是負數的情況
		if(maxsofar == 0){
			maxsofar = p[0];
			for(int i = 1; i < length; i++){
				if(p[i] > maxsofar){
					maxsofar = p[i];
					maxbegin = i;
					maxend = i;
				}
			}
		}
		for (int i = maxbegin; i <= maxend; i++) {
			System.out.println("i=" + p[i]);
		}
	}

	public static void main(String[] args) {
		int p[] = new int[]{2,4,-7,5,2,-1,2,-4,3};
		maxSumSubArray(p, p.length);
	}
}
複製程式碼

執行結果

> i=5
> i=2
> i=-1
> i=2
複製程式碼

2.3 連續子陣列的最大和(二維)

問題描述

這個問題其實是2.2的變種,這時候輸入是一個二維的矩陣,需要找到一個子矩陣,該子矩陣的和是這個二維的所有子矩陣中最大的。

解決思路

二維的問題和2.2中的一維問題的核心解決思路相同。

對於二維情況,我們將 同一列的多個元素合併成一個元素 來實現降維的效果,為了能實現在O(1)的時間內計算出同一列的多行元素之和,需要構建一個輔助陣列,該輔助陣列ps[i][j]的值,等於原輸入陣列pp[0][0]為左上角到p[i][j]為右下角構成的子矩陣的所有元素之和,通過該輔助陣列就能在O(1)的時間內計算出lRowhRow行之間第col列的所有元素之和,計算公式為:

ps[hRow][col] - ps[hRow][col-1] - ps[lRow-1][col] + ps[lRow-1][col-1]
複製程式碼

程式碼實現

class Untitled {
    
    //計算從lRow到hRow之間,位於第col列的所有元素之和。
    static int getColsum(int ps[][], int lRow, int hRow, int col) {
        return ps[hRow][col] - ps[hRow][col-1] - ps[lRow-1][col] + ps[lRow-1][col-1];
    }

    static void maxSumSubArray2(int p[][], int xlen, int ylen){
        int maxendinghere = 0;
        int maxsofar = 0;
        int tColbegin = 1;
        int sx = 0;
        int sy = 0;
        int ex = 0;
        int ey = 0;
        //初始化輔助陣列,ps[i][j]為以其為右下角的子矩陣的所有元素之和。
        int psxLen = xlen+1;
        int psyLen = ylen+1;
        int[][] ps = new int[psxLen][psyLen];
        for (int j = 0; j < psyLen; j++)
            ps[0][j] = 0;
        for (int i = 0; i < psxLen; i++)
            ps[i][0] = 0;
        for (int i = 1; i < psxLen; i++) {
            for(int j = 1; j < psyLen; j++) {
                ps[i][j] = ps[i-1][j] + ps[i][j-1] - ps[i-1][j-1] + p[i-1][j-1];
            }
        }
        //求矩陣中的最大和,將位於同一個列的多行元素合併成一個元素,因此需要遍歷包含不同行的情況。
        for (int pStartRow = 1; pStartRow < psxLen; pStartRow++) { 
            for (int pEndRow = pStartRow; pEndRow < psxLen; pEndRow++) {
                for (int pCol = 1; pCol < psyLen; pCol++) {
                    maxendinghere += getColsum(ps, pStartRow, pEndRow, pCol);
                    if (maxendinghere < 0) {
                        maxendinghere = 0;
                        tColbegin = pCol+1;
                    }
                    if (maxsofar < maxendinghere) {
                        maxsofar = maxendinghere;
                        sx = pStartRow - 1;
                        sy = tColbegin - 1;
                        ex = pEndRow - 1;
                        ey = pCol - 1;
                    }
                }
                maxendinghere = 0;
                tColbegin = 1;
            }
        }
		System.out.println("最大和=" + maxsofar + ",起始行=" + sx + ",終止行=" + ex + ",起始列=" + sy + ",終止列=" + ey);
    }

    public static void main(String[] args) {
		int[][] p = {{1,-10,-11}, {4,5,6}, {7,8,9}};
        maxSumSubArray2(p, 3, 3);
    }
}
複製程式碼

執行結果

>> 最大和=39,起始行=1,終止行=2,起始列=0,終止列=2
複製程式碼

2.4 求陣列中的逆序對

問題描述

在陣列中的兩個數字,如果 前面一個數字大於後面的數字,則這兩個數字組成一個 逆序對。輸入一個陣列,求出這個陣列中的逆序對的總數。

解決思路

這裡採用的是 歸併演算法 的思想,歸併演算法包含三個關鍵步驟:

  • 分解:把長度為n的待排序列分解成兩個長度為n/2的序列。
  • 治理:對每個子序列分別呼叫歸併排序,進行遞迴操作。當子序列長度為1時,序列本身有序,停止遞迴。
  • 合併:合併每個排序好的子序列。

對於上面的例子,我們將整個陣列分解為A、B兩部分,則整個陣列的逆序對個數就等於:

A部分組成的陣列的逆序對 + B部分組成的陣列的逆序對 + A與B之間的逆序對
複製程式碼

這裡有一個關鍵的點,就是需要保證在計算AB之間的逆序對時,AB內的元素都是有序的。

class Untitled {
	
	static int inversePairs(int p[], int startIndex, int endIndex) {
		if (endIndex == startIndex) {
		    return 0;
		}
		if (endIndex-startIndex == 1) {
		    if (p[endIndex] < p[startIndex]) {
				int temp = p[startIndex];
				p[startIndex] = p[endIndex];
				p[endIndex] = temp;
				return 1;
			} else {
			    return 0;
			}
		}
		int midOffset = (endIndex-startIndex) >> 1;
		int l = inversePairs(p, startIndex, startIndex+midOffset);
		int r = inversePairs(p, startIndex+midOffset+1, endIndex);
		return l + r + inverseCore(p, startIndex, midOffset, endIndex);
	}
	
	static int inverseCore(int p[], int startIndex, int midOffset, int endIndex) {
		int totalLen = endIndex-startIndex+1;
	    int lLen = midOffset+1;
		int rLen = totalLen-lLen;
		int l[] = new int[lLen+1];
		int r[] = new int[rLen+1];
		int i = 0;
		for (i=0; i<lLen; i++) {
		    l[i] = p[startIndex+i];
		}
		l[i] = 1 << 30;
		for (i=0; i<rLen; i++) {
		    r[i] = p[startIndex+lLen+i];
		}
		r[i] = 1 << 30;
		int c = 0;
		i = 0;
		int m = 0;
		int n = 0;
		while(i < totalLen) {
			if (r[n] <= l[m]) {
				p[startIndex+i] = r[n];
				c += (lLen - m);
				n++;
				i++;
			} else {
			    p[startIndex+i] = l[m];
				m++;
				i++;
			}
		}
		return c;
	}
	public static void main(String[] args) {
		int[] p = {7,5,6,4};
		System.out.println("Inverse Count=" + inversePairs(p, 0, 3));
	}
}
複製程式碼

執行結果

>> Inverse Count=5
複製程式碼

更多文章,歡迎訪問我的 Android 知識梳理系列:

相關文章