代碼面試之鏈表


  最近經歷了各種面試,各種東北西跑,各種面試准備,以及各種各樣不一樣的問題和面試官對自己本科大學的打擊,,我相信即使是普通大學也會讓考官眼前一亮的,,作為一名非211的湘潭大學的學子,我不知道自己以后路在何方,但依然會繼續前行,相信自己會做到最好!

  剛從武漢面試回來,打算總結一下面試經驗,身為一名通信工程專業的與技術行離不開的技術男,肯定離不開各種電路設計,程序設計,總之在面試過程中,考官也會問道關於數據結構這種最常見的問題,話不多說,先就總結一下吧!

  鏈表是最基本的數據結構,鏈表相關的操作相對而言比較簡單,在面試過程時間有限的過程當中也適合考察寫代碼的能力。鏈表的操作離不開指針,而指針又很容易導致出錯。綜合多方面的原因,鏈表題目在面試中占據着很重要的地位,甚至可以說必不可少,在我看來也是學習指針的最好辦法,是理解C語言最好的學習方式。

  下面我先來列出自己敲的代碼,有關於靜態鏈表的創建,都有自己詳細的解釋,大家先看看了解一下:

typedef struct _tag_StaticListNode  //結點結構體定義 
{
    unsigned int data;      //
    int next;    			//保存鏈表里的數據元素 
} TStaticListNode;

typedef struct _tag_StaticList	//靜態鏈表 結構體定義 
{
    int capacity;	//靜態鏈表最多容納的元素 
    TStaticListNode header;  //鏈表頭 
    TStaticListNode node[]; //柔性數組 
} TStaticList;

StaticList* StaticList_Create(int capacity) // O(n)-- 1.創建靜態鏈表 
{
    TStaticList* ret = NULL;    //定義返回值變量ret 
    int i = 0;
    
    if( capacity >= 0 )	//指定的容量大為0,要合法 
    {
        ret = (TStaticList*)malloc(sizeof(TStaticList) + sizeof(TStaticListNode) * (capacity + 1));
   			//malloc動態的申明空間來申明動態鏈表以及里面的數組, (capacity + 1)因為有一個被設為表頭 
    }
    
    if( ret != NULL )
    {
        ret->capacity = capacity;//賦初值 
        ret->header.data = 0;	//	
        ret->header.next = 0;   //開始一個元素都沒有故為0 
        
        for(i=1; i<=capacity; i++)
        {
            ret->node[i].next = AVAILABLE;//所有 位置標志為可用 
        }
    }
    
    return ret;
}

void StaticList_Destroy(StaticList* list) // O(1)  直接釋放free 
{
    free(list);
}

void StaticList_Clear(StaticList* list) // O(n)   清除 
{
    TStaticList* sList = (TStaticList*)list;   //強制類型轉換 
    int i = 0;
    
    if( sList != NULL )   //判斷合法 
    {
        sList->header.data = 0;
        sList->header.next = 0;    //先清空 ,因為所有元素的下標都是大於0 的 
        
        for(i=1; i<=sList->capacity; i++)
        {
            sList->node[i].next = AVAILABLE;  //所有的數組都是可以使用的 
        }
    }
}

int StaticList_Length(StaticList* list) // O(1)
{
    TStaticList* sList = (TStaticList*)list;
    int ret = -1;	//表示傳進來的數據不合法 
    
    if( sList != NULL )   //判斷是否合法,再賦值 
    {
        ret = sList->header.data;  //把得到的值賦給ret 
    }
    
    return ret;
}

int StaticList_Capacity(StaticList* list) // O(1)返回最大容量 
{
    TStaticList* sList = (TStaticList*)list;
    int ret = -1;
    
    if( sList != NULL )
    {
        ret = sList->capacity;
    }
    
    return ret;
}

int StaticList_Insert(StaticList* list, StaticListNode* node, int pos)  // O(n)
//進行插入一個元素的操作 
{
    TStaticList* sList = (TStaticList*)list; //強制轉換做一個數據封裝 
    int ret = (sList != NULL); //1代表插入成功,0代表插入失敗 
    int current = 0;
    int index = 0;
    int i = 0;
    
    ret = ret && (sList->header.data + 1 <= sList->capacity);//插入位置是否合法的 
    ret = ret && (pos >=0) && (node != NULL); //插入位置補位空且大於0 
    
    if( ret )  // ret = 1時 
    {
        for(i=1; i<=sList->capacity; i++)   //開始尋找哪個位置可用 
        {
            if( sList->node[i].next == AVAILABLE )  //判斷是否可用 
            {
                index = i;   //記下插入位置 
                break;
            }
        }
        
        sList->node[index].data = (unsigned int)node;//新元素放到這個位置來 
        
        sList->node[0] = sList->header; //表頭 
        
        for(i=0; (i<pos) && (sList->node[current].next != 0); i++)//移動pos次,(此位置下標不為0) 
        {
            current = sList->node[current].next; //移到下一個元素的下標 
        }
        
        sList->node[index].next = sList->node[current].next;//1. 新元素與下一個元素連接 
        sList->node[current].next = index;//2.新元素與插入位置前一個位置連接 
        
        sList->node[0].data++; //元素長度加1了 
        
        sList->header = sList->node[0];  //更新表頭信息 
    }
    
    return ret;
}

StaticListNode* StaticList_Get(StaticList* list, int pos)  // O(n)
{
    TStaticList* sList = (TStaticList*)list;
    StaticListNode* ret = NULL;  //定義返回值變量 
    int current = 0; //表頭的下標為0 
    int object = 0; //要獲取的元素下標 
    int i = 0;
    
    if( (sList != NULL) && (0 <= pos) && (pos < sList->header.data) )
    //sLIST指針不為空 , 獲取元素位置pos>0  並且小於它的長度值 
	{
        sList->node[0] = sList->header; //第0個元素就是頭結點 
        
        for(i=0; i<pos; i++)
        {
            current = sList->node[current].next; 
        }
        
        object = sList->node[current].next;//當前元素的next域即為要獲取元素在數組中的下標 
        
        ret = (StaticListNode*)(sList->node[object].data);//獲取的值 
    }
    
    return ret;
}

StaticListNode* StaticList_Delete(StaticList* list, int pos) // O(n)刪除第pos元素 
{
    TStaticList* sList = (TStaticList*)list;
    StaticListNode* ret = NULL;
    int current = 0;
    int object = 0;
    int i = 0;
    
    if( (sList != NULL) && (0 <= pos) && (pos < sList->header.data) )
    {
        sList->node[0] = sList->header;
        
        for(i=0; i<pos; i++)
        {
            current = sList->node[current].next;
        }
        
        object = sList->node[current].next;
        
        sList->node[current].next = sList->node[object].next; // 與增加一個元素一樣 
        
        sList->node[0].data--;  //元素刪除,減1 
        
        sList->header = sList->node[0];  //更新表頭信息 
        
        sList->node[object].next = AVAILABLE;  //可用 
        
        ret = (StaticListNode*)(sList->node[object].data);//數據返回 
    }
    
    return ret;
}

  而對於循環鏈表來說,其實和靜態鏈表是一個道理,只是在基於單向鏈表的基礎上有一些改動,這里我只是把改動的列出來,當然其余的可能還是有一些小的差異,但是這個並不明顯,下面是改動的部分代碼,筆者也做了相關解釋:

int CircleList_Insert(CircleList* list, CircleListNode* node, int pos) // O(n)
//循環鏈表中要是插入第一個元素的話就得另加說明了 
{ 
    TCircleList* sList = (TCircleList*)list;
    int ret = (sList != NULL) && (pos >= 0) && (node != NULL);
    int i = 0;
    
    if( ret )
    {
        CircleListNode* current = (CircleListNode*)sList;
        
        for(i=0; (i<pos) && (current->next != NULL); i++)
        {
            current = current->next;
        }
        
        node->next = current->next;
        current->next = node;
        
        if( sList->length == 0 )   //插入的是第一個元素 
        {
            sList->slider = node;
            node->next = node;   //插入元素得指向自己才能構成循環 
        }
        
        sList->length++;
    }
    
    return ret;
}

CircleListNode* CircleList_Get(CircleList* list, int pos) // O(n)獲取元素 
{
    TCircleList* sList = (TCircleList*)list;
    CircleListNode* ret = NULL;
    int i = 0;
    
    if( (sList != NULL) && (pos >= 0) )//不需要(pos<sList->length)這個約束 
    {
        CircleListNode* current = (CircleListNode*)sList;
        
        for(i=0; i<pos; i++)
        {
            current = current->next;
        }
        
        ret = current->next;
    }
    
    return ret;
}

CircleListNode* CircleList_Delete(CircleList* list, int pos) // O(n)
{
    TCircleList* sList = (TCircleList*)list;
    CircleListNode* ret = NULL;
    int i = 0;
    
    if( (sList != NULL) && (pos >= 0) )
    {
        CircleListNode* current = (CircleListNode*)sList;
        CircleListNode* first = sList->header.next; //first指向鏈表的 第一個元素 
        CircleListNode* last = (CircleListNode*)CircleList_Get(sList, sList->length - 1);
        
        for(i=0; i<pos; i++)  //開始移動 
        {
            current = current->next;
        }
        
        ret = current->next;    
        current->next = ret->next;
        
        sList->length--;
        
        if( first == ret )  //如果刪除的是第一個元素 
        {
            sList->header.next = ret->next;  //將表頭指向ret 
            last->next = ret->next;			//最后一個指針移動到第二個位置上 
        }
        
        if( sList->slider == ret )//游標 
        {
            sList->slider = ret->next;
        }
        
        if( sList->length == 0 )  //鏈表是否為空 
        {
            sList->header.next = NULL;
            sList->slider = NULL;
        }
    }
    
    return ret;
}

  至於雙向鏈表,就復雜一些了,它的每個數據結點中都有兩個指針,分別指向直接后繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和后繼結點,這樣代碼難度就加大了,其實也是一個意思,但是在短短的面試時間中,我覺得還是很少讓你寫出來的,一般理解了單鏈表就可以應付了,下面我詳細的介紹在面試過程當中會問道的有關鏈表的知識。

1 求單鏈表的節點數,注意檢查鏈表是否為空。時間復雜度為O(n)

// 求單鏈表中結點的個數
unsigned int GetListLength(ListNode * pHead)
{
	if(pHead == NULL)
		return 0;

	unsigned int nLength = 0;
	ListNode * pCurrent = pHead;
	while(pCurrent != NULL)
	{
		nLength++;
		pCurrent = pCurrent->m_pNext;
	}
	return nLength;
}

2 將單鏈表翻轉,從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)

// 反轉單鏈表
ListNode * ReverseList(ListNode * pHead)
{
        // 如果鏈表為空或只有一個結點,無需反轉,直接返回原鏈表頭指針
	if(pHead == NULL || pHead->m_pNext == NULL)  
		return pHead;

	ListNode * pReversedHead = NULL; // 反轉后的新鏈表頭指針,初始為NULL
	ListNode * pCurrent = pHead;
	while(pCurrent != NULL)
	{
		ListNode * pTemp = pCurrent;
		pCurrent = pCurrent->m_pNext;
		pTemp->m_pNext = pReversedHead; // 將當前結點摘下,插入新鏈表的最前端
		pReversedHead = pTemp;
	}
	return pReversedHead;
}

3 查找單鏈表中的第n個點(n>0),使用兩個指針,先讓前面的指針走到正向第k個結點,這樣前后兩個指針的距離差是k-1,之后前后兩個指針一起向前走,前面的指針走到最后一個結點時,后面指針所指結點就是倒數第k個結點。

// 查找單鏈表中倒數第K個結點
ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函數名前面的R代表反向
{
	if(k == 0 || pHead == NULL) // 這里k的計數是從1開始的,若k為0或鏈表為空返回NULL
		return NULL;

	ListNode * pAhead = pHead;
	ListNode * pBehind = pHead;
	while(k > 1 && pAhead != NULL) // 前面的指針先走到正向第k個結點
	{
		pAhead = pAhead->m_pNext;
		k--;
	}
	if(k > 1 || pAhead == NULL)     // 結點個數小於k,返回NULL
		return NULL;
	while(pAhead->m_pNext != NULL)  // 前后兩個指針一起向前走,直到前面的指針指向最后一個結點
	{
		pBehind = pBehind->m_pNext;
		pAhead = pAhead->m_pNext;
	}
	return pBehind;  // 后面的指針所指結點就是倒數第k個結點
}

4 查找單鏈表中的單節點,設置兩個指針,兩個指針同時向前走,前面的指針每次走兩步,后面的指針每次走一步,前面的指針走到最后一個結點時,后面的指針所指結點就是中間結點,即第(n/2+1)個結點。注意鏈表為空,鏈表結點個數為1和2的情況。時間復雜度O(n)

// 獲取單鏈表中間結點,若鏈表長度為n(n>0),則返回第n/2+1個結點
ListNode * GetMiddleNode(ListNode * pHead)
{
	if(pHead == NULL || pHead->m_pNext == NULL) // 鏈表為空或只有一個結點,返回頭指針
		return pHead;

	ListNode * pAhead = pHead;
	ListNode * pBehind = pHead;
	while(pAhead->m_pNext != NULL) // 前面指針每次走兩步,直到指向最后一個結點,后面指針每次走一步
	{
		pAhead = pAhead->m_pNext;
		pBehind = pBehind->m_pNext;
		if(pAhead->m_pNext != NULL)
			pAhead = pAhead->m_pNext;
	}
	return pBehind; // 后面的指針所指結點即為中間結點
}

5 從尾部到頭部打印單鏈表,就是倒序打印單鏈表!注意鏈表為空的情況,下面使用的是棧。時間復雜度為O(n)

// 從尾到頭打印鏈表,使用棧
void RPrintList(ListNode * pHead)
{
	std::stack<ListNode *> s;
	ListNode * pNode = pHead;
	while(pNode != NULL)
	{
		s.push(pNode);
		pNode = pNode->m_pNext;
	}
	while(!s.empty())
	{
		pNode = s.top();
		printf("%d\t", pNode->m_nKey);
		s.pop();
	}
}
// 從尾到頭打印鏈表,使用遞歸
void RPrintList(ListNode * pHead)
{
	if(pHead == NULL)
	{
		return;
	}
	else
	{
		RPrintList(pHead->m_pNext);
		printf("%d\t", pHead->m_nKey);
	}
}

6 已知兩個單鏈表pHead1 和pHead2 各自有序,把它們合並成一個鏈表依然有序,需要O(1)的空間。時間復雜度為O(max(len1, len2))

// 合並兩個有序鏈表
ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2)
{
	if(pHead1 == NULL)
		return pHead2;
	if(pHead2 == NULL)
		return pHead1;
	ListNode * pHeadMerged = NULL;
	if(pHead1->m_nKey < pHead2->m_nKey)
	{
		pHeadMerged = pHead1;
		pHeadMerged->m_pNext = NULL;
		pHead1 = pHead1->m_pNext;
	}
	else
	{
		pHeadMerged = pHead2;
		pHeadMerged->m_pNext = NULL;
		pHead2 = pHead2->m_pNext;
	}
	ListNode * pTemp = pHeadMerged;
	while(pHead1 != NULL && pHead2 != NULL)
	{
		if(pHead1->m_nKey < pHead2->m_nKey)
		{
			pTemp->m_pNext = pHead1;
			pHead1 = pHead1->m_pNext;
			pTemp = pTemp->m_pNext;
			pTemp->m_pNext = NULL;
		}
		else
		{
			pTemp->m_pNext = pHead2;
			pHead2 = pHead2->m_pNext;
			pTemp = pTemp->m_pNext;
			pTemp->m_pNext = NULL;
		}
	}
	if(pHead1 != NULL)
		pTemp->m_pNext = pHead1;
	else if(pHead2 != NULL)
		pTemp->m_pNext = pHead2;
	return pHeadMerged;
}

7 判斷一個單鏈表是否有環!如果一個鏈表中有環,也就是說用一個指針去遍歷,是永遠走不到頭的。因此,我們可以用兩個指針去遍歷,一個指針一次走兩步,一個指針一次走一步,如果有環,兩個指針肯定會在環中相遇。時間復雜度為O(n)

bool HasCircle(ListNode * pHead)
{
	ListNode * pFast = pHead; // 快指針每次前進兩步
	ListNode * pSlow = pHead; // 慢指針每次前進一步
	while(pFast != NULL && pFast->m_pNext != NULL)
	{
		pFast = pFast->m_pNext->m_pNext;
		pSlow = pSlow->m_pNext;
		if(pSlow == pFast) // 相遇,存在環
			return true;
	}
	return false;
}

8 判斷兩個單鏈表是否相交:如果兩個鏈表相交於某一節點,那么在這個相交節點之后的所有節點都是兩個鏈表所共有的。也就是說,如果兩個鏈表相交,那么最后一個節點肯定是共有的。先遍歷第一個鏈表,記住最后一個節點,然后遍歷第二個鏈表,到最后一個節點時和第一個鏈表的最后一個節點做比較,如果相同,則相交,否則不相交。時間復雜度為O(len1+len2),因為只需要一個額外指針保存最后一個節點地址,空間復雜度為O(1)

bool IsIntersected(ListNode * pHead1, ListNode * pHead2)
{
        if(pHead1 == NULL || pHead2 == NULL)
                return false;

	ListNode * pTail1 = pHead1;
	while(pTail1->m_pNext != NULL)
		pTail1 = pTail1->m_pNext;

	ListNode * pTail2 = pHead2;
	while(pTail2->m_pNext != NULL)
		pTail2 = pTail2->m_pNext;
	return pTail1 == pTail2;
}

9 求兩個單鏈表相交的第一個節點:兩個鏈表均從頭節點開始,假設len1大於len2,那么將第一個鏈表先遍歷len1-len2個節點,此時兩個鏈表當前節點到第一個相交節點的距離就相等了,然后一起向后遍歷,知道兩個節點的地址相同。時間復雜度,O(len1+len2)

ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2)
{
	if(pHead1 == NULL || pHead2 == NULL)
		return NULL;

	int len1 = 1;
	ListNode * pTail1 = pHead1;
	while(pTail1->m_pNext != NULL)
	{
		pTail1 = pTail1->m_pNext;
		len1++;
	}

	int len2 = 1;
	ListNode * pTail2 = pHead2;
	while(pTail2->m_pNext != NULL)
	{
		pTail2 = pTail2->m_pNext;
		len2++;
	}

	if(pTail1 != pTail2) // 不相交直接返回NULL
		return NULL;

	ListNode * pNode1 = pHead1;
	ListNode * pNode2 = pHead2;
        // 先對齊兩個鏈表的當前結點,使之到尾節點的距離相等
	if(len1 > len2)
	{
		int k = len1 - len2;
		while(k--)
			pNode1 = pNode1->m_pNext;
	}
	else
	{
		int k = len2 - len1;
		while(k--)
			pNode2 = pNode2->m_pNext;
	}
	while(pNode1 != pNode2)
	{
		pNode1 = pNode1->m_pNext;
		pNode2 = pNode2->m_pNext;
	}
        return pNode1;
}

10 已知一個單鏈表中存在環,求進入環中的第一個節點:首先判斷是否存在環,若不存在結束。在環中的一個節點處斷開(當然函數結束時不能破壞原鏈表),這樣就形成了兩個相交的單鏈表,求進入環中的第一個節點也就轉換成了求兩個單鏈表相交的第一個節點。

ListNode* GetFirstNodeInCircle(ListNode * pHead)
{
	if(pHead == NULL || pHead->m_pNext == NULL)
		return NULL;

	ListNode * pFast = pHead;
	ListNode * pSlow = pHead;
	while(pFast != NULL && pFast->m_pNext != NULL)
	{
		pSlow = pSlow->m_pNext;
		pFast = pFast->m_pNext->m_pNext;
		if(pSlow == pFast)
			break;
	}
	if(pFast == NULL || pFast->m_pNext == NULL)
		return NULL;

	// 將環中的此節點作為假設的尾節點,將它變成兩個單鏈表相交問題
	ListNode * pAssumedTail = pSlow; 
	ListNode * pHead1 = pHead;
	ListNode * pHead2 = pAssumedTail->m_pNext;

	ListNode * pNode1, * pNode2;
	int len1 = 1;
	ListNode * pNode1 = pHead1;
	while(pNode1 != pAssumedTail)
	{
		pNode1 = pNode1->m_pNext;
		len1++;
	}
	
	int len2 = 1;
	ListNode * pNode2 = pHead2;
	while(pNode2 != pAssumedTail)
	{
		pNode2 = pNode2->m_pNext;
		len2++;
	}

	pNode1 = pHead1;
	pNode2 = pHead2;
	// 先對齊兩個鏈表的當前結點,使之到尾節點的距離相等
	if(len1 > len2)
	{
		int k = len1 - len2;
		while(k--)
			pNode1 = pNode1->m_pNext;
	}
	else
	{
		int k = len2 - len1;
		while(k--)
			pNode2 = pNode2->m_pNext;
	}
	while(pNode1 != pNode2)
	{
		pNode1 = pNode1->m_pNext;
		pNode2 = pNode2->m_pNext;
	}
    return pNode1;
}

11 給出一單鏈表頭指針pHead和一節點指針pToBeDeleted,O(1)時間復雜度刪除節點pToBeDeleted

void Delete(ListNode * pHead, ListNode * pToBeDeleted)
{
	if(pToBeDeleted == NULL)
		return;
	if(pToBeDeleted->m_pNext != NULL)
	{
		pToBeDeleted->m_nKey = pToBeDeleted->m_pNext->m_nKey; // 將下一個節點的數據復制到本節點,然后刪除下一個節點
		ListNode * temp = pToBeDeleted->m_pNext;
		pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext;
		delete temp;
	}
	else // 要刪除的是最后一個節點
	{
		if(pHead == pToBeDeleted) // 鏈表中只有一個節點的情況
		{
			pHead = NULL;
			delete pToBeDeleted;
		}
		else
		{
			ListNode * pNode = pHead;
			while(pNode->m_pNext != pToBeDeleted) // 找到倒數第二個節點
				pNode = pNode->m_pNext;
			pNode->m_pNext = NULL;
			delete pToBeDeleted;
		}	
	}
}

  

  版權所有,轉載請注明轉載地址:http://www.cnblogs.com/lihuidashen/p/4809784.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM