1. 程式人生 > >程式設計師面試題目總結--陣列(三)【旋轉陣列的最小數字、旋轉陣列中查詢指定數、兩個排序陣列所有元素中間值、陣列中重複次數最多的數、陣列中出現次數超過一半的數】

程式設計師面試題目總結--陣列(三)【旋轉陣列的最小數字、旋轉陣列中查詢指定數、兩個排序陣列所有元素中間值、陣列中重複次數最多的數、陣列中出現次數超過一半的數】

11、求旋轉陣列的最小數字

題目:輸入一個排好序的陣列的一個旋轉,輸出旋轉陣列的最小元素。

分析:陣列的旋轉:把一個數組最開始的若干個元素搬到陣列的末尾。例如陣列{3, 4, 5, 1, 2}{1, 2, 3, 4, 5}的一個旋轉,該陣列的最小值為1。這道題最直觀的解法並不難。從頭到尾遍歷陣列一次,就能找出最小的元素,時間複雜度顯然是O(N)。但這個思路沒有利用輸入陣列的特性,我們應該能找到更好的解法。

我們注意到旋轉之後的陣列實際上可以劃分為兩個排序的子陣列,而且前面的子陣列的元素都大於或者等於後面子陣列的元素。我們還可以注意到最小的元素剛好是這兩個子陣列的分界線。我們試著用二元查詢法的思路在尋找這個最小的元素。

首先我們用兩個指標,分別指向陣列的第一個元素和最後一個元素。按照題目旋轉的規則,第一個元素應該是大於或者等於最後一個元素的(這其實不完全對,還有特例。後面再討論特例)。

接著我們得到處在陣列中間的元素。如果該中間元素位於前面的遞增子陣列,那麼它應該大於或者等於第一個指標指向的元素。此時陣列中最小的元素應該位於該中間元素的後面。我們可以把第一指標指向該中間元素,這樣可以縮小尋找的範圍。同樣,如果中間元素位於後面的遞增子陣列,那麼它應該小於或者等於第二個指標指向的元素。此時該陣列中最小的元素應該位於該中間元素的前面。我們可以把第二個指標指向該中間元素,這樣同樣可以縮小尋找的範圍。我們接著再用更新之後的兩個指標,去得到和比較新的中間元素,迴圈下去。

按照上述的思路,我們的第一個指標總是指向前面遞增陣列的元素,而第二個指標總是指向後面遞增陣列的元素。最後第一個指標將指向前面子陣列的最後一個元素,而第二個指標會指向後面子陣列的第一個元素。也就是它們最終會指向兩個相鄰的元素,而第二個指標指向的剛好是最小的元素。這就是迴圈結束的條件。

注:

1、前面提到在旋轉陣列中,由於是把遞增排序陣列前面的若干數字搬到陣列的後面,因此第一個數字總是大於或等於最後一個數字,但有個特例:把排序陣列前面的0個元素搬到後面,即陣列沒有改變,還是原來的排序陣列。此時,陣列中第一個數字就是最小數字。

2、當兩個指標指向的數字和中間數字相同時,無法判斷中間的數字式位於前面的子陣列中還是後面的子陣列中,則上面的方法就失效了。此時,則使用順序查詢方法。

bool glo_InvalidInput=false;
int MinInOrder(int* numbers, int index1, int index2);

int Min(int* numbers, int length)
{
	if(numbers == NULL || length <= 0)
	{
		glo_InvalidInput=true;
		return -1;
	}

	int index1 = 0;
	int index2 = length - 1;
	int indexMid = index1;
	while(numbers[index1] >= numbers[index2])
	{
		// 如果index1和index2指向相鄰的兩個數,
		// 則index1指向第一個遞增子陣列的最後一個數字,
		// index2指向第二個子陣列的第一個數字,也就是陣列中的最小數字
		if(index2 - index1 == 1)
		{
			indexMid = index2;
			break;
		}

		// 如果下標為index1、index2和indexMid指向的三個數字相等,
		// 則只能順序查詢
		indexMid = (index1 + index2) / 2;
		if(numbers[index1] == numbers[index2] && numbers[indexMid] == numbers[index1])
			return MinInOrder(numbers, index1, index2);

		// 縮小查詢範圍
		if(numbers[indexMid] >= numbers[index1])
			index1 = indexMid;
		else if(numbers[indexMid] <= numbers[index2])
			index2 = indexMid;
	}

	return numbers[indexMid];
}

int MinInOrder(int* numbers, int index1, int index2)
{
	int result = numbers[index1];
	for(int i = index1 + 1; i <= index2; ++i)
	{
		if(result > numbers[i])
			result = numbers[i];
	}

	return result;
}

12、在旋轉陣列中查詢指定的數(無重複元素)

題目:在旋轉陣列中查詢指定的數,找到返回下標,否則返回-1

分析:陣列的旋轉:把一個數組最開始的若干個元素搬到陣列的末尾。例如陣列{3, 4, 5, 1, 2}{1, 2, 3, 4, 5}

我們注意到旋轉之後的陣列實際上可以劃分為兩個排序的子陣列,可以試著用二元查詢法的思路在尋找指定的元素。

/************************************************************************
* 陣列中沒有重複數字                                                                    
************************************************************************/
int SearchRotatedArray(int a[], int n, int value)
{
	if(a==NULL || n <=0)
		return -1;
	int begin=0, end=n-1;
	while(begin!=end)
	{
		int mid=(begin+end)/2;
		if(value == a[mid])
			return mid;
		if(a[begin] < a[mid])
		{
			if(a[begin] <= value && value < a[mid])
				end=mid;
			else
				begin=mid;
		}
		else
		{
			if(a[mid] < value && value <=a[end])
				begin=mid;
			else
				end=mid;
		}
	}
	return -1;
}

擴充套件:上面題目中若允許有重複數字如何處理?

分析:
    允許重複元素,則上一題中如果A[m]>=A[l], 那麼[l,m] 為遞增序列的假設就不能成立了,比
    如[1,3,1,1,1]。
    如果A[m]>=A[l] 不能確定遞增,那就把它拆分成兩個條件:
    • 若A[m]>A[l],則區間[l,m] 一定遞增
    • 若A[m]==A[l] 確定不了,那就l++,往下看一步即可。

/************************************************************************
* 陣列中存在重複數字                                                                     
************************************************************************/
int SearchRotatedArray1(int a[], int n, int value)
{
	if(a==NULL || n <=0)
		return -1;
	int begin=0, end=n-1;
	while(begin!=end)
	{
		int mid=(begin+end)/2;
		if(value == a[mid])
			return mid;
		if(a[begin] < a[mid])
		{
			if(a[begin] <= value && value < a[mid])
				end=mid;
			else
				begin=mid;
		}
		else if(a[begin] > a[mid])
		{
			if(a[mid] < value && value <=a[end])
				begin=mid;
			else
				end=mid;
		}
		else
			begin++;

	}
	return -1;
}

13、求兩個排序陣列中的所有元素中間值

題目:給定兩個已經排序的陣列,找出兩者所有元素中的中間值

分析:這是一道非常經典的題。這題更通用的形式是,給定兩個已經排序好的陣列,找到兩者所有元素中第k 大的元素。
O(m + n) 的解法比較直觀,直接merge 兩個陣列,然後求第k 大的元素。
不過我們僅僅需要第k 大的元素,是不需要“排序”這麼複雜的操作的。可以用一個計數器,記錄當前已經找到第m 大的元素了。同時我們使用兩個指標pA 和pB,分別指向A 和B 陣列的第一個元素,使用類似於merge sort 的原理,如果陣列A 當前元素小,那麼pA++,同時m++;如果陣列B 當前元素小,那麼pB++,同時m++。最終當m 等於k 的時候,就得到了我們的答案,O(k)時間,O(1) 空間。但是,當k 很接近m + n 的時候,這個方法還是O(m + n) 的。
有沒有更好的方案呢?我們可以考慮從k 入手。如果我們每次都能夠刪除一個一定在第k 大元素之前的元素,那麼我們需要進行k 次。但是如果每次我們都刪除一半呢?由於A 和B 都是有序的,我們應該充分利用這裡面的資訊,類似於二分查詢,也是充分利用了“有序”。
假設A 和B 的元素個數都大於k/2,我們將A 的第k/2 個元素(即A[k/2-1])和B 的第k/2個元素(即B[k/2-1])進行比較,有以下三種情況(為了簡化這裡先假設k 為偶數,所得到的結論對於k 是奇數也是成立的):
• A[k/2-1] == B[k/2-1]
• A[k/2-1] > B[k/2-1]
• A[k/2-1] < B[k/2-1]
如果A[k/2-1] < B[k/2-1],意味著A[0] 到A[k/2-1 的肯定在A U B 的top k 元素的範圍內,換句話說,A[k/2-1]不可能大於A U B 的第k 大元素。留給讀者證明。
因此,我們可以放心的刪除A 陣列的這k/2 個元素。同理,當A[k/2-1] > B[k/2-1] 時,可以刪除B 陣列的k/2 個元素。
當A[k/2-1] == B[k/2-1] 時,說明找到了第k 大的元素,直接返回A[k/2-1] 或B[k/2-1]即可。
因此,我們可以寫一個遞迴函式。那麼函式什麼時候應該終止呢?
• 當A 或B 是空時,直接返回B[k-1] 或A[k-1];
• 當k=1 是,返回min(A[0], B[0]);
• 當A[k/2-1] == B[k/2-1] 時,返回A[k/2-1] 或B[k/2-1]

bool glo_InvalidInput=false;
int find_kth(int A[], int m, int B[], int n, int k)
{
		//always assume that m is equal or smaller than n
		if (m > n) return find_kth(B, n, A, m, k);
		if (m == 0) return B[k - 1];
		if (k == 1) return min(A[0], B[0]);
		//divide k into two parts
		int ia = min(k / 2, m), ib = k - ia;
		if (A[ia - 1] < B[ib - 1])
			return find_kth(A + ia, m - ia, B, n, k - ia);
		else if (A[ia - 1] > B[ib - 1])
			return find_kth(A, m, B + ib, n - ib, k - ib);
		else
			return A[ia - 1];
	}

double findMedianSortedArrays(int A[], int m, int B[], int n) 
{
	if(A == NULL || B==NULL || (m <= 0 && n <=0))
	{
		glo_InvalidInput=true;
		return -1;
	}
	int total = m + n;
	if (total & 0x1)
		return find_kth(A, m, B, n, total / 2 + 1);
	else
		return (find_kth(A, m, B, n, total / 2)
		+ find_kth(A, m, B, n, total / 2 + 1)) / 2.0;
}

14、找出陣列中重複次數最多的數

題目:給定一個數組,找出陣列中出現重複次數最多的數

分析:

方法一:以空間換時間,可以定義一個數組 int count[MAX],並將其陣列元素都初始化為0,然後執行for(int i=0;i<n;i++) count[A[i]]++;在count中找最大的數,即為重複次數最多的數。

int GetMaxNum(int a[], int n ,int& num)
{
	int index=a[0];
	for(int i=0;i<n;i++)
	{
		if(a[i]>index)
		{
			index=a[i];
			num=i;
		}
	}
	return index;
}
int main()
{
	int a[]={1,1,2,2,4,4,4,4,5,5,6,6};
	int len=sizeof(a)/sizeof(a[0]);
	int num=0;
	int n=GetMaxNum(a,len,num);
	int *count = new int[n+1];
	for(int i=0;i<n;i++)
		count[i]=0;
	for(int i=0;i<len;i++)
		count[a[i]]++;
	cout << "重複的次數:" << GetMaxNum(count,n,num) << endl;
	cout << "重複次數最多的數:"<< num << endl;
	delete[] count;
}
方法二:使用map對映,通過引入map表來記錄每一個元素出現的次數,然後判斷次數的大小,進而找出重複次數最多的元素
//找出陣列中重複次數最多的數
#include<iostream>
#include<map>
using namespace std;

bool FindMostFrequentNum(int a[],int n, int& val, int& num)
{
	if(a==NULL || n<=0)
		return false;
	map<int,int> m;
	for(int i=0;i<n;i++)
	{
		if(++m[a[i]] > m[val])
			val=a[i];
	}
	num=m[val];
	return true;
}

int main()
{
	int a[]={1,5,4,3,4,4,5,4,4,5,6};
	int len=sizeof(a)/sizeof(a[0]);
	int val=0;
	int num=0;
	if(FindMostFrequentNum(a,len,val,num))
	{
		cout << "重複的次數:" << num << endl;
		cout << "重複次數最多的數:"<< val << endl;
	}
}

15、求陣列中出現次數超過一半的數

題目:在O(n)時間複雜度內找出陣列中出現次數超過一半的數

分析:每次取出兩個不同的數,剩下的數字中重複出現的數字肯定比其他數字多,將規模縮小化,如果每次刪除兩個不同的數,那麼在剩餘的數字裡,原最高頻數出現的頻率一樣超過了50%,不斷重複這個過程,最後剩下的將全是同樣的數字,即最高頻數。

//在O(n)時間複雜度內找出陣列中出現次數超過一半的數
int  Find(int a[], int n)
{
	if(a==NULL || n <=0)
		return -1;

	int candidate=0;
	int count=0;
	for(int i=0;i<n;i++)
	{
		if(count==0)
		{
			candidate=a[i];
			count=1;
		}
		else
		{
			if(candidate==a[i])
				count++;
			else
				count--;
		}
	}

	count=0;
	for(int i=0;i<n;i++)
	{
		if(a[i]==candidate)
			count++;
	}
	if(count*2 <= n)
		return -1;

	return candidate;
}