本文由玉剛說寫作平臺提供寫作贊助,版權(quán)歸玉剛說所有。
原作者:Jiantao
版權(quán)聲明:未經(jīng)玉剛說許可,不得以任何形式轉(zhuǎn)載。
數(shù)據(jù)結(jié)構(gòu)和算法有多重要?
我想有追求的程序員都不會放過它的。 打個比方,在金庸的武俠世界里,數(shù)據(jù)結(jié)構(gòu)和算法它就像一門上乘的內(nèi)功心法,一旦掌握了它,各種武功信手拈來,毫無壓力(張無忌就是一個典型的例子);對于程序員來說,它能決定你在技術(shù)這條道路上能走多遠。
本文主要涉及數(shù)組、字符串、List這幾個數(shù)據(jù)結(jié)構(gòu),然后通過解答和分析幾道常見的面試題,從中分享一些我的學(xué)習(xí)心得和解題套路,希望對你有幫助。
題目1:翻轉(zhuǎn)句子
題目: 給定一個英文句子,每個單詞之間都是由一個或多個空格隔開,請翻轉(zhuǎn)句子中的單詞順序(包括空格的順序),但單詞內(nèi)字符的順序保持不變。例如輸入"www google com ",則應(yīng)輸出" com google www"。
如果你經(jīng)常關(guān)注算法相關(guān)文章,這道題應(yīng)該會比較熟悉,各種博客和書籍上都有出現(xiàn),不熟悉也沒關(guān)系,現(xiàn)在我們就一起來嘗試解答下。這里要注意題意和網(wǎng)上流傳的題目有個不同點:網(wǎng)上基本都是單詞間有且只有一個空格。而此題需要考慮一個或多個空格的情況。
解題思路
試想一下,如果將整個字符串翻轉(zhuǎn),結(jié)果是句子是反轉(zhuǎn)了,但單詞內(nèi)的字符順序也翻轉(zhuǎn)了。如果要保證單詞內(nèi)順序不變,只需要再將每個單詞翻轉(zhuǎn)一下就滿足要求了。
由于題中“www google com ”字符串較長,我就以" hello world"為例分析下這個過程,請看下圖。
圖 1.0 翻轉(zhuǎn)句子,但保證句子中單詞內(nèi)部字符順序。
注:(1)字符串" hello world"初始狀態(tài),注意首字符是空格。 (2)將" hello world"整個句子翻轉(zhuǎn)后的樣子??梢钥闯霾粌H翻轉(zhuǎn)了句子中單詞的順序(包括空格),連單詞內(nèi)的字符順序也翻轉(zhuǎn)了。(3) 定義兩個指針p1、p2都指向句子的首字符。 (4)首字符d,不是空格,此時p1指針不動,p2指針向右移動1位,指向字符 l。(移動p2指針目的:檢查單詞的結(jié)束位置。) (5)由于第二個字符為 l ,也不是空格,p2繼續(xù)向右移動1位。(6)多次移動后,p2指針在第一個空格處停下來,此時就能得知p2-1為該單詞的結(jié)束位置。(7)反轉(zhuǎn)兩個指針(p1、p2-1)中間的字符串。(8)交換后,重置兩個指針位置p1=p2++。以此類推,繼續(xù)尋找下一個單詞并翻轉(zhuǎn),直到指針移動到句子末尾就結(jié)束循環(huán)。
此思路的關(guān)鍵是:1. 實現(xiàn)一個函數(shù)/方法,翻轉(zhuǎn)字符串中的一段。 2. 判斷并獲取句子中的單詞,注意空格。
測試用例
- 功能測試:多個單詞、1個單詞、單詞間只有一個空格、單詞間有多個空格。
- 特殊輸入測試:空字符、字符串中只有空格、null對象(指針)。
編碼實現(xiàn)
- Java代碼
/**
* @param chars 原字符串
* @param start 大于等于0
* @param end 小于 length
* @return
*/
private char[] v1_0_reverse(char[] chars, int start, int end) {
// str 判斷null, 索引有效值判斷
if (chars == null || start < 0 || end >= chars.length || start >= end) {
return chars;
}
while (start < end) {
// 收尾字符互換,直到替換完成。
char temp = chars[start];
chars[start] = chars[end];
chars[end] = temp;
start++;
end--;
}
return chars;
}
private String v1_0_solution(String sentence) {
if (sentence == null || sentence.isEmpty()) {
return sentence;
}
int length = sentence.length();
// 第一步翻轉(zhuǎn)所有字符
char[] chars = v1_0_reverse(sentence.toCharArray(), 0, length - 1);
System.out.println(new String(chars));
// 第二步翻轉(zhuǎn)每個單詞(重點:怎么找到單詞)
int start = 0, end = 0;
while (start < length) {
if (chars[start] == ' ') {
// 遇到空格就向右邊繼續(xù)查找
start++;
end++;
} else if (end == length || chars[end] == ' ') {
// 遇到空格或者已經(jīng)到了字符串末尾,此時翻轉(zhuǎn)找到的單詞內(nèi)部字符,這里需要注意end-1
chars = v1_0_reverse(chars, start, end - 1);
System.out.println(new String(chars));
// 重新制定檢查索引start
start = end++;
} else {
// end加1,為了檢查單詞是否結(jié)束
end++;
}
}
return new String(chars);
}
- C++ 代碼實現(xiàn)
// 反轉(zhuǎn)字符串
void Reverse(char *pBegin, char *pEnd)
{
if(pBegin == NULL || pEnd == NULL)
return;
while(pBegin < pEnd)
{
char temp = *pBegin;
*pBegin = *pEnd;
*pEnd = temp;
pBegin ++, pEnd --;
}
}
// 翻轉(zhuǎn)句子中單詞順序,但保證單詞內(nèi)字符順序不變。
char* ReverseSentence(char *pData)
{
if(pData == NULL)
return NULL;
char *pBegin = pData;
char *pEnd = pData;
while(*pEnd != '\0')
pEnd ++;
pEnd--;
// 翻轉(zhuǎn)整個句子
Reverse(pBegin, pEnd);
// 翻轉(zhuǎn)句子中的每個單詞
pBegin = pEnd = pData;
while(*pBegin != '\0')
{
if(*pBegin == ' ')
{
pBegin ++;
pEnd ++;
}
else if(*pEnd == ' ' || *pEnd == '\0')
{
Reverse(pBegin, --pEnd);
pBegin = ++pEnd;
}
else
{
pEnd ++;
}
}
return pData;
}
如果你在面試的時候遇到這道題,并且很容易就想到了這個算法,有經(jīng)驗的面試官就會在這道題基礎(chǔ)上加點難度,繼續(xù)考查面試者。so,第二道題來了:
題目:接上題,面試官繼續(xù)提問,我們得到的" com google www"需要被用作一個URL的參數(shù),所以這里需要的處理是去掉開頭結(jié)尾的無效空格,并將兩個單詞中間的每一個空格都替換為"%20"。例如" com google www"應(yīng)被轉(zhuǎn)換為"com%20%20google%20www",請給出轉(zhuǎn)換函數(shù)。
解題思路
- 第一步去掉收尾的無效空格;比如" com google www"去掉后得到"com google www"。
- 第二步將兩個單詞中間的每一個空格都替換為"%20"。
還是以" hello world"為例,簡單分析下解題過程,請看下圖。
圖 1.1 剔除收尾無效空格,并將單詞間的每一個空格都替換為"%20"。
注:(1)字符串" hello world",這里注意首字符是空格。 (2)剔除首尾空格后。 (3)對原字符串進行擴容。newLen = len + 2 x blackCount;這里解釋下新數(shù)組的長度是如何計算的,由于是將每一個空格都替換為"%20",就相當(dāng)于原來占一個字符替換后要占三個字符,換言之,每一個空格就會多出兩個字符長度,所以就有前面的表達式。 (4) 定義兩個指針p1、p2,分別指向len-1和newLen-1位置。 (5)判斷p1指針是否指向空格,如果是則在p2處開始插入字符“%20”,不是則將p1指向的值復(fù)制給p2并將兩個指針往左移動一位。這里將p1指向的字符 d 賦值給p2,并將兩個指針向左移動一位。 (6)將p1指向的字符 l 賦值給p2,并移動指針。 (7)多次賦值和移動后,p1指向了第一個空格。 (8)在p2處依次插入字符 0 、 2 、 % ,并指針p2向左移動三位,結(jié)束后將p1向左移動一位,此時p1、p2重合結(jié)束循環(huán)。
測試用例
- 功能測試:前后有無空格情況、中間一個或多個空格情況。
- 特殊輸入測試:空字符、字符串中只有空格、null對象(指針)。
編碼實現(xiàn)
- Java代碼
private String v1_1_solution(String sentence) {
if (sentence == null || sentence.isEmpty()) {
return sentence;
}
// 去掉字符串收尾的空格
sentence = trim(sentence);
int len = sentence.length();
char[] chars = sentence.toCharArray();
int count = getSpaceCount(sentence);
int newLen = 2 * count + len;
// 擴容,內(nèi)部使用System.arraycopy 方法實現(xiàn)。
chars = Arrays.copyOf(chars, newLen);
int index = len - 1;
int newIndex = newLen - 1;
while (index >= 0 && newIndex > index) {
if (chars[index] == ' ') {
chars[newIndex--] = '0';
chars[newIndex--] = '2';
chars[newIndex--] = '%';
} else {
chars[newIndex--] = chars[index];
}
index--;
}
return new String(chars);
}
/**
* 剔除字符串收尾的空格
*
* @param origin
* @return
*/
private String trim(String origin) {
char[] chars = origin.toCharArray();
int length = chars.length;
int st = 0;
while (st < length && chars[st] == ' ') {
st++;
}
while (st < length && chars[length - 1] == ' ') {
length--;
}
// 如果收尾有空格,就截取生成新的字符串
if (st > 0 || length < chars.length) {
origin = new String(chars, st, (length - st));
}
return origin;
}
private int getSpaceCount(String sentence) {
char[] chars = sentence.toCharArray();
int count = 0;
for (char c : chars) {
if (c == ' ') {
count++;
}
}
return count;
}
- C++實現(xiàn)
/* 去掉收尾空格:將原字符串截取后返回新字符串 */
void trim(char *strIn, char *strOut){
int i = 0;
int j = strlen(strIn) - 1;
while(strIn[i] == ' ')
++i;
while(strIn[j] == ' ')
--j;
strncpy(strOut, strIn + i , j - i + 1);
strOut[j - i + 1] = '\0';
}
/*length 為字符數(shù)組string的總?cè)萘?/
void replaceBlank(char string[], int length)
{
if(string == NULL && length <= 0)
return;
/*originalLength 為字符串string的實際長度*/
int originalLength = 0;
int numberOfBlank = 0;
int i = 0;
while(string[i] != '\0')
{
++ originalLength;
if(string[i] == ' ')
++ numberOfBlank;
++ i;
}
/*newLength 為把空格替換成'%20'之后的長度*/
int newLength = originalLength + numberOfBlank * 2;
if(newLength > length)
return;
int indexOfOriginal = originalLength;
int indexOfNew = newLength;
while(indexOfOriginal >= 0 && indexOfNew > indexOfOriginal)
{
if(string[indexOfOriginal] == ' ')
{
string[indexOfNew --] = '0';
string[indexOfNew --] = '2';
string[indexOfNew --] = '%';
}
else
{
string[indexOfNew --] = string[indexOfOriginal];
}
-- indexOfOriginal;
}
}
題目2:調(diào)整數(shù)組中元素順序
題目: 給定一個整數(shù)數(shù)組,請實現(xiàn)一個函數(shù)來調(diào)整數(shù)組中數(shù)字的順序,使得所有奇數(shù)都位于偶數(shù)之前。
解題思路
此題比較簡單,我最先想到的解法是這樣:我們維護兩個指針(索引),一個指針指向數(shù)組的第一個數(shù)字,稱之為頭指針,向右移動;一個指針指向最后一個數(shù)字,稱之為尾指針,向左移動。
圖2.0 調(diào)整數(shù)組{2,1,3,6,4,7,8,5}使得奇數(shù)位于偶數(shù)前面的過程。
注:(1)初始化兩個指針P1、P2,分別指向數(shù)組的頭部和尾部。(2)由上一步得知,指針P1指向的數(shù)字是偶數(shù)2,而P2指向的數(shù)字是奇數(shù)5,滿足條件,我們交換這兩個數(shù)字。(3) P1繼續(xù)向右移動直到指向偶數(shù)6,P2繼續(xù)向左移動直到指向奇數(shù)7。(4)交換兩個指針指向的數(shù)字。(5)P1,P2繼續(xù)移動后重疊,表明所有奇數(shù)已位于偶數(shù)前面了。
循環(huán)結(jié)束條件:兩個指針重疊時或P2指針移動到了P1指針的前面,此時退出循環(huán)。
可以看出此算法,一次循環(huán)搞定,所以時間復(fù)雜度O(n), 由于在原數(shù)組上操作,所以空間復(fù)雜度O(1)。
測試用例
- 功能測試:全是奇數(shù)、全是偶數(shù)、奇偶數(shù)存在但已排好序/未排好序。
- 特殊輸入測試: null對象、數(shù)組元素為0、有負數(shù)情況。
編碼
- Java實現(xiàn)
private int[] v2_0_solution(int[] nums) {
if (nums == null || nums.length <= 1) {
return nums;
}
int st = 0;
int end = nums.length - 1;
while (st < end) {
// find even number
if (isOdd(nums[st])) {
st++;// 奇數(shù),索引右移
} else if (!isOdd(nums[end])) {
end--;// 偶數(shù),索引左移
} else {
// 奇偶數(shù)互換
int temp = nums[st];
nums[st] = nums[end];
nums[end] = temp;
st++;
end--;
}
}
return nums;
}
// 與1做按位運算,不為0就是奇數(shù),反之為偶數(shù)
private boolean isOdd(int n) {
return (n & 1) != 0;
}
- C++實現(xiàn)
// 互換
void swap(int* num1, int* num2)
{
int temp = *num1;
*num1 = *num2;
*num2 = temp;
}
//判斷是否為奇數(shù)
bool isOdd(int data)
{
return (data & 1) != 0;
}
//奇偶互換
void oddEvenSort(int *pData, unsigned int length)
{
if (pData == NULL || length == 0)
return;
int *pBegin = pData;
int *pEnd = pData + length - 1;
while (pBegin < pEnd)
{
//如果pBegin指針指向的是奇數(shù),正常,向右移
if (isOdd(*pBegin))
{
pBegin++;
}
//如果pEnd指針指向的是偶數(shù),正常,向左移
else if (!isOdd(*pEnd))
{
pEnd--;
}
else
{
//否則都不正常,交換
swap(pBegin, pEnd);
}
}
}
有經(jīng)驗的面試官又來了,題目難度需要升下級,??~
題目: 接上題,面試官會繼續(xù)要求改造此函數(shù)使其能夠保證原先輸入數(shù)組的奇數(shù)內(nèi)部順序以及偶數(shù)內(nèi)部順序,即如果輸入為{2,1,3,6,4,7,8,5},則輸出應(yīng)為{1,3,7,5,2,6,4,8},奇數(shù)之間的相互順序和偶數(shù)之間的相互順序不得被改變。
解題思路
要想保證原數(shù)組內(nèi)元素的順序,可使用O(n)的temp數(shù)組空間按順序緩存偶數(shù),奇數(shù)依次放到原數(shù)組前面,最后將temp中偶數(shù)取出放在原數(shù)組后面。
圖 2.1 借助O(n)的temp數(shù)組緩存偶數(shù),進而保證原數(shù)組順序。
注: 變量解釋:st為即將插入的奇數(shù)在原數(shù)組中的索引,evenCount為緩存的偶數(shù)個數(shù)。(1)初始化和原數(shù)組相同長度的數(shù)組temp,指針p1指向首個元素,st=eventCount=0。 (2)將p1指向的偶數(shù) 2 放入在temp中,evenCount自加1。 (3)由于p1指針向右移動一位指向的是奇數(shù) 1 ,所以將p1指向的值賦值給Array[st],此時還st=0,賦值完成后st自加1。 (8)依次邏輯,直到循環(huán)結(jié)束時,已完成原數(shù)組中奇數(shù)元素按順序插入到了頭部,偶數(shù)按順序緩存在了temp數(shù)組中,即圖中狀態(tài)。
上圖展示了偶數(shù)按順序緩存到temp數(shù)組中,奇數(shù)按順序放到原數(shù)組前面。最后將temp數(shù)組中的偶數(shù)依次按序放在原數(shù)組后面,這個過程較簡單,就沒體現(xiàn)到圖中,具體請看下面代碼實現(xiàn)。
測試用例
同上一題。這里就省略了。
編碼
- Java實現(xiàn)
private int[] v2_1_solution(int[] nums) {
if (nums == null || nums.length <= 1) {
return nums;
}
int st = 0;
int evenCount = 0;
int[] temp = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
if (!isOdd(nums[i])) {
evenCount += 1;
temp[evenCount - 1] = nums[i];
} else {
if (st < i) {
// 將奇數(shù)依次放在原數(shù)組前面
nums[st] = nums[i];
}
st++;
}
}
if (evenCount > 0) {
for (int i = st; i < nums.length; i++) {
nums[i] = temp[i - st];
}
}
return nums;
}
- C++實現(xiàn)
void v2_1_solution(int* nums,unsigned int len)
{
if (!nums || len <= 1) {
return;
}
int st = 0;
int evenCount = 0;
// 申請的內(nèi)存空間temp
int temp[len];
for (int i = 0; i < len; i++) {
if (!isOdd(nums[i])) {
evenCount += 1;
temp[evenCount - 1] = nums[i];
} else {
if (st < i) {
// 將奇數(shù)依次放在原數(shù)組前面
nums[st] = nums[i];
}
st++;
}
}
// 將temp中偶數(shù)取出放在原數(shù)組后面
if (evenCount > 0) {
for (int i = st; i < len; i++) {
nums[i] = temp[i - st];
}
}
}
題目3:利用數(shù)組實現(xiàn)一個簡易版的List
題目:請利用數(shù)組實現(xiàn)一個簡易版的List,需要實現(xiàn)poll和push兩個接口,前者為移除并獲得隊頭元素,后者為向隊尾添加一個元素,并要求能夠自動擴容。
解題思路
還是以“hello world”為例,作圖分析下。
圖 3.0 List的push和poll過程
注:(1) 初始化List,數(shù)組默認容量len為8,size=0。(容量小一點方便作圖,實際容量看需求而定。) (2) 隊尾添加字符 h ,size++。 (3)添加len-1個字符后,size指向數(shù)組最后一個位置。 (4)如果再添加字符 O ,由于size++滿足條件:大于等于len,此時需要先對List先擴容,擴容后,再進行添加字符操作。 (5) 接著繼續(xù)添加,直到“hello world”都push到List中。 (6)這是一個poll過程,可以看出即獲取了對頭元素 h ,并且整個數(shù)組中元素向左移動一位來實現(xiàn)移除效果。
關(guān)于擴容:每次擴容多少?上圖例子是變?yōu)樵瓉淼?倍。像ArrayList則是這樣 int newCapacity = oldCapacity + (oldCapacity >> 1),可以看出擴容后大小 = 原來大小 + 原來大小/2。所以擴容多少由你自己決定。
此題關(guān)鍵是在怎么實現(xiàn)poll和push兩個接口上。
- push(添加元素):按索引添加到數(shù)組中,size大于等于數(shù)組長度時就先擴容。
- poll(獲取并移動對頭元素):移動數(shù)組并置空最后一個元素。
測試用例
- 功能測試: 添加、移除元素
- 特殊測試: 添加大量數(shù)據(jù)(測試擴容)、移除所有元素、null數(shù)據(jù)
編碼
- Java實現(xiàn)
private static final int DEFAULT_CAPACITY = 16;
private Object[] elementData;
// 實際存儲的元素數(shù)量
// The size of the List (the number of elements it contains).
private int size;
public CustomList() {
elementData = new Object[DEFAULT_CAPACITY];
}
public CustomList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = new Object[DEFAULT_CAPACITY];
} else {
throw new IllegalArgumentException("Illegal Capacity: " +
initialCapacity);
}
}
/**
* 移除并獲得隊頭元素
*
* @return
*/
public Object poll() {
if (size <= 0){
throw new IndexOutOfBoundsException(" list is empty .");
}
// 獲取隊頭第一個元素
Object oldValue = elementData[0];
// 數(shù)組元素左移一位 & 最后一位元素置空
System.arraycopy(elementData, 1, elementData, 0, size - 1);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
/**
* 向隊尾添加一個元素
*
* @param item
*/
public void push(Object item) {
ensureExplicitCapacity(size + 1); // Increments modCount!!
elementData[size++] = item;
}
@Override
public String toString() {
return Arrays.toString(elementData);
}
// 這里擴容參考的ArrayList,具體實現(xiàn)請點擊文末源代碼鏈接前往查看。
private void ensureExplicitCapacity(int minCapacity) {
// 期望的最小容量大于等于現(xiàn)有數(shù)組的長度,則進行擴容
if (minCapacity - elementData.length >= 0)
grow(minCapacity);
}
- C++實現(xiàn)
class List {
private:
int expansionSize = 16;//每次擴容個數(shù)
int elemsSize = 0;//數(shù)組長度
int dataIndex = -1;//最后一位元素下標(biāo)
T* elems; //元素
public:
List(){
elemsSize = 0;
dataIndex = -1;
}
List(int initialCapacity){
if (initialCapacity<=0) {
throw out_of_range("initialCapacity must > 0");
}
elemsSize = initialCapacity;
elems = new T[initialCapacity];
}
void push(T const&); // 入棧
T poll(); // 出棧
int size();
~List(){
if(elemsSize>0){
delete []elems;
}
}
};
template <class T>
void List<T>::push (T const& elem)
{
if(elemsSize <= 0){//初始化數(shù)組
elemsSize = expansionSize;
elems = new T[elemsSize];
}
if(dataIndex+1 >= elemsSize){//數(shù)組擴容
elemsSize += expansionSize;
T* newElems = new T[elemsSize];
for(int i=0;i<=dataIndex;i++){
newElems[i] = elems[i];
}
delete[]elems;
elems = newElems;
}
dataIndex++;
elems[dataIndex] = elem;
}
template <class T>
T List<T>::poll ()
{
if (dataIndex<0) {
throw out_of_range("List<>::poll(): empty List");
}
T poll = elems[0]; //獲取第一位
for(int i=1;i<=dataIndex;i++){//后面元素向左移
elems[i-1] = elems[i];
}
dataIndex--;
return poll;
}
template <class T>
int List<T>::size ()
{
return dataIndex+1;
}
題目4:數(shù)組中出現(xiàn)次數(shù)超過一半的數(shù)
題目: 一個整數(shù)數(shù)組中有一個數(shù)字出現(xiàn)的次數(shù)超過了數(shù)組長度的一半,請找出這個數(shù)字。如輸入一個長度為9的數(shù)組{1,2,3,2,2,2,5,4,2},由于2出現(xiàn)了5次,超過了數(shù)組長度的一半,因此應(yīng)輸出2。
解題思路
如果我們將數(shù)組排序,那么排序后位于數(shù)組中間的的數(shù)字一定是那個出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字。這個數(shù)就是統(tǒng)計學(xué)上的中位數(shù)。
此題關(guān)鍵在于快速排序算法,我們一起看看下面這張圖,來理解下快排的思想。
圖 4.0 快速排序過程動圖,圖片來源Wikipedia。
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。
-
步驟為:
- 從數(shù)列中挑出一個元素,稱為"基準(zhǔn)"(pivot),
- 重新排序數(shù)列,所有比基準(zhǔn)值小的元素擺放在基準(zhǔn)前面,所有比基準(zhǔn)值大的元素擺在基準(zhǔn)后面(相同的數(shù)可以到任何一邊)。在這個分區(qū)結(jié)束之后,該基準(zhǔn)就處于數(shù)列的中間位置。這個稱為分區(qū)(partition)操作。
- 遞歸地(recursively)把小于基準(zhǔn)值元素的子數(shù)列和大于基準(zhǔn)值元素的子數(shù)列排序。
遞歸到最底部時,數(shù)列的大小是零或一,也就是已經(jīng)排序好了。這個算法一定會結(jié)束,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最后的位置去。
測試用例
- 存在(或者不存在)次數(shù)超過數(shù)組長度一半的數(shù)。
- 特殊用例: null、空元素、 只有一個數(shù)。
編碼
- Java實現(xiàn)
private int v4_0_solution(int[] array) {
if (array == null || array.length < 1) {
throw new IllegalArgumentException(" array is empty. ");
}
int head = 0;
int tail = array.length - 1;
// 快速排序
qSort(array, head, tail);
int middle = array.length >> 1;
int result = array[middle];
// 判斷中位數(shù)是否為超過數(shù)組長度一半的數(shù)。
if (checkMoreThanHalf(array, result)) {
return result;
} else {
throw new IllegalArgumentException("not find the number.");
}
}
public void qSort(int[] arr, int head, int tail) {
// 參數(shù)合法性及結(jié)束條件
if (head >= tail || arr == null || arr.length <= 1) {
return;
}
// 取中間數(shù)為基準(zhǔn)值
int i = head, j = tail, pivot = arr[(head + tail) / 2];
while (i <= j) {
// 處理大于等于基準(zhǔn)數(shù)情況
while (arr[i] < pivot) {
++i;
}
while (arr[j] > pivot) {
--j;
}
// 直接互換,沒有基準(zhǔn)數(shù)歸位操作
if (i < j) {
swap(arr, i, j);
++i;
--j;
} else if (i == j) {
++i;
}
}
// 遞歸處理基準(zhǔn)數(shù)分隔的兩個子數(shù)列。
qSort(arr, head, j);
qSort(arr, i, tail);
}
private boolean checkMoreThanHalf(int[] nums, int number) {
int times = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == number) {
times++;
}
}
return times * 2 > nums.length;
}
- C++ 實現(xiàn)
// 快速排序:遞歸方式 參考Wikipedia
void quick_sort_recursive(int arr[], int start, int end) {
if (start >= end)
return;
int mid = arr[end];
int left = start, right = end - 1;
while (left < right) {
while (arr[left] < mid && left < right)
left++;
while (arr[right] >= mid && left < right)
right--;
std::swap(arr[left], arr[right]);
}
if (arr[left] >= arr[end])
std::swap(arr[left], arr[end]);
else
left++;
quick_sort_recursive(arr, start, left - 1);
quick_sort_recursive(arr, left + 1, end);
}
int main()
{
//存在出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字
//int data[] = {1, 2, 3, 2, 2, 2, 5, 4, 2};
//不存在出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字
//int data[] = {4, 5, 1, 6, 2, 7, 3, 8};
// 出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字都出現(xiàn)在數(shù)組的前/后半部分
int data[] = {2, 2, 2, 2, 2, 1, 3, 4, 5};
//int data[] = {1, 3, 4, 5, 2, 2, 2, 2, 2};
int len = sizeof(data)/sizeof(int);
printf("length = %d \n", len);
quick_sort_recursive(data, 0, len -1);
for(int i=0;i<len;i++){
printf(" %d ", data[i]);
}
printf("\n");
int middle = len >> 1;
int result = data[middle];
if(CheckMoreThanHalf(data, len, result)){
printf("the number is %d ", result);
}else{
printf("not find the number.");
}
return 0;
}
有經(jīng)驗的面試官又來了,題目難度需要升下級,??~
題目:這個題目有很多變種,其中一個引申為輸入的是一個對象數(shù)組,該對象無法比較大小,只能利用equals()方法比較是否相等,此時該如何解(若要用到O(n)的輔助空間,能否避免?)。
解題思路
數(shù)組中有一個元素出現(xiàn)的次數(shù)超過數(shù)組長度的一半,也就是說它出現(xiàn)的次數(shù)比其他所有元素出現(xiàn)次數(shù)的和還要多。
因此我們可以考慮在遍歷數(shù)組的時候保存兩個值: 一個是數(shù)組中的一個元素, 一個是次數(shù)。當(dāng)我們遍歷到下一個元素的時候,如果下一個元素和我們之前保存的元素相等(equals返回true),則次數(shù)加1;如果下一個元素和我們之前保存的不相等,則次數(shù)減1。如果次數(shù)為0,我們需要保存下一個元素,并把次數(shù)設(shè)為1。由于我們要找的數(shù)字出現(xiàn)的次數(shù)比其他所有數(shù)字出現(xiàn)的次數(shù)之和還要多,那么要找的數(shù)字肯定是最后一次把次數(shù)設(shè)為1時對應(yīng)的那個元素。
怎么樣簡單吧,還是畫張圖來理解一下。
圖4.0 數(shù)組中出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)。
注:雖然途中數(shù)組元素類型是整型,但其思想適用于任何類型。(1) 數(shù)組初始狀態(tài),times只是一個標(biāo)記變量,默認為0, result為最后一次設(shè)置times=1時的那個元素,默認為NULL。(2)開始循環(huán),i=0時,times設(shè)置為1,并將第一個元素 1 賦值給result變量。 (3)i=1時,由于此時Array[i]的值為 2 ,不等于result,所以times--,操作后times等于0,result不變。(4)i=2時,由于此時times==0,所以重新設(shè)置times=1,result= Array[2]= 3 。(5)i=3時,和(3)類似,由于此時Array[i]的為2,不等于result,所以times--,操作后times等于0,result不變還是等于3。(6)依次邏輯,一直遍歷到末尾,即i=8時,邏輯同上,可以求出times=1,result=2;ok,循環(huán)結(jié)束。
到這里得出result=2,那這個2是不是我們要找的那個元素呢? 答案是:不一定。 如果輸入數(shù)組中存在次數(shù)超過超過數(shù)組長度一半的數(shù),那result就是那個數(shù),否則就不是。所以,我們還需要對這個數(shù)進行檢查,檢查過程請參看下方代碼。
此思路:空間復(fù)雜度O(1),時間復(fù)雜度O(n)。
編碼
- Java實現(xiàn)
private Object v4_1_solution(Object[] objects) {
if (objects == null || objects.length < 1) {
throw new IllegalArgumentException(" array is empty. ");
}
// 假設(shè)第一個元素就是超過長度一半的那個
Object result = objects[0];
int times = 1;
// 從第二個元素開始遍歷
for (int i = 1; i < objects.length; i++) {
if (times == 0) {
// 重新設(shè)置
result = objects[i];
times = 1;
} else if (objects[i].equals(result)) {
times++;
} else {
times--;
}
}
if (checkMoreThanHalf(objects, result)) {
return result;
} else {
throw new IllegalArgumentException(" array is invalid ");
}
}
private boolean checkMoreThanHalf(Object[] objects, Object obj) {
int times = 0;
for (int i = 0; i < objects.length; i++) {
if (objects[i].equals(obj)) {
times++;
}
}
return times * 2 > objects.length;
}
// 測試類,重點在于實現(xiàn)了equals和hashcode方法。
private static class TestObject {
String unique;
public TestObject(String unique) {
this.unique = unique;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestObject that = (TestObject) o;
return unique != null ? unique.equals(that.unique) : that.unique == null;
}
@Override
public int hashCode() {
return unique != null ? unique.hashCode() : 0;
}
@Override
public String toString() {
return "TestObject{" +
"unique='" + unique + '\'' +
'}';
}
}
- C++實現(xiàn)
template <class T>
class Array {
private:
bool checkMoreThanHalf(T *objects,unsigned int len,T obj)
{
unsigned int times = 0;
for (int i = 0; i < len; i++) {
if (objects[i] == obj) {
times++;
}
}
return times * 2 > len;
};
public:
T v4_1_solution(T *objects,unsigned int len);
};
template <class T>
T Array<T>::v4_1_solution (T *objects,unsigned int len)
{
if (!objects || len < 1) {
throw out_of_range(" array is empty. ");
}
// 假設(shè)第一個元素就是超過長度一半的那個
T result = objects[0];
if(len == 1){
return result;
}
int times = 1;
// 從第二個元素開始遍歷
for (int i = 1; i < len; i++) {
if (times == 0) {
// 重新設(shè)置
result = objects[i];
times = 1;
} else if (objects[i] == result) {
times++;
} else {
times--;
}
}
if (checkMoreThanHalf(objects,len, result)) {
return result;
} else {
throw out_of_range(" array is invalid ");
}
}
學(xué)習(xí)心得&解題套路
細心的讀者可能發(fā)現(xiàn)了,文中解題過程大致是這樣的:分析思路->測試用例->編碼->調(diào)試并通過測試。你可能會問怎樣才能很好的掌握算法編程呢?我的建議是:有事沒事刷道題吧。勤加練習(xí),終成大神。哈哈,請輕拍。
-
關(guān)于解題思路(詳見劍指offer)
- 畫圖讓抽象問題形象化
- 舉例讓抽象問題具體化
- 分解讓復(fù)雜問題簡單化
-
學(xué)習(xí)資源(信息大爆炸,好資源很重要)
- 各種數(shù)據(jù)結(jié)構(gòu)及算法書籍: 大話數(shù)據(jù)結(jié)構(gòu)、劍指offer、算法導(dǎo)論等等。
- 在線編程:LeetCode、??途W(wǎng)、七月在線
總結(jié)
現(xiàn)在去大公司面試,都會有算法題,所以不是你想不想掌握它,而是公司會通過它把一部分人淘汰掉,說的可能有點嚇人,但現(xiàn)實就是這樣操作的。文中所有代碼均編譯運行并通過測試用例檢查,由于篇幅限制,代碼沒有貼全,完整的可運行代碼請點擊鏈接獲?。?https://github.com/yangjiantao/DSAA。 由于作者水平有限,文中錯誤之處在所難免,敬請讀者指正。
編程能力就像任何其他技能一樣,也是一個可以通過刻意練習(xí)大大提高的。 --- 摘抄至LeetCode。