c++ 数据结构和算法之刷无聊的面试题(1)-单链表
文章目錄
- (一)鏈表節點ListNode類~
- (二)單鏈表MyForward_List類~
- 1.構造函數 MyForward_List()~
- 2.刪除鏈表中所有數據的函數 clear()~
- 3.析構函數 ~MyForward_List()~
- 4.頭插法插入元素 push_front(T data)~
- 5.頭刪法刪除元素 pop_front()~
- 6.尾插法插入元素 push_back(T data)~
- 7.尾插法刪除元素 pop_back()~
- 8.按data刪除鏈表中其中一個元素 erase(T data)
- 9.刪除符合data值的所有元素 remove(T data)
- 10.找到倒數第k個節點-解法1 Find_K_Node(const int& k)
- 11.找到倒數第k個節點-解法2 Find_K_Node2(const int& k)
- 12.反轉鏈表 reverse()
- 13.正向輸出單鏈表 PrintList()
- 14.反向遍歷輸出單鏈表 PrintReverseList()
- 15.獲取鏈表的首元素 front()
- 16.得到鏈表的長度 getSize()
- (三)非成員函數騷操作鏈表題
- 1.合并兩個排序的鏈表
- 2.找兩個鏈表最近公共的節點解法1
- 3.找兩個鏈表最近公共的節點解法2
- (四)后序可能添加的
(一)鏈表節點ListNode類~
template<typename T> struct ListNode {T _data;ListNode<T>* _next;ListNode() :_data(0), _next(nullptr){} };(二)單鏈表MyForward_List類~
(在下面再寫出其中的函數,方便起見,不用分文件編寫的方式),所有函數方便起見,全部設定為public
template<typename T> class MyForward_List { private:ListNode<T>* _head;int _size;//鏈表的大小 };1.構造函數 MyForward_List()~
注意:鑒于我目前看到的c++鏈表面試題的解法,基本頭節點都不是空哨兵節點,所以此處,只要鏈表不為空,_head均有意義,并非是空哨兵節點(即_head的data沒意義,也不會去訪問,其只有一個next指針指向鏈表首節點)。(實際上最好有一個空哨兵作為首節點,如果有空哨兵,有的函數能寫的更加簡便點)
MyForward_List(){_head = nullptr;_size = 0;}2.刪除鏈表中所有數據的函數 clear()~
由于所有數據在插入時,都會采用在堆區創建的形式,所以需要一個函數來清空所有堆區的數據,因此也需要遍歷一遍單鏈表。
/*清楚所有數據*/void clear(){//要遍歷一遍,將所有數據刪除。ListNode<T>* next = nullptr;//當前要被刪除節點的后繼節點for (ListNode<T>* curr = _head; curr != nullptr;){next = curr->_next;delete curr;curr = next;}_size = 0;}3.析構函數 ~MyForward_List()~
~MyForward_List(){clear();}4.頭插法插入元素 push_front(T data)~
void push_front(T data){if (_head == nullptr)//如果頭節點為空,則將此節點設為頭節點{_head = new ListNode<T>();_head->_data = data;_head->_next = nullptr;}else{ListNode<T>* oldHead = new ListNode<T>();//創建新節點,其值為原head的值。oldHead->_data = _head->_data;oldHead->_next = _head->_next;_head->_data = data;_head->_next = oldHead;}_size++;}5.頭刪法刪除元素 pop_front()~
任何刪除函數,都需要考慮到鏈表是否為空的情況。
void pop_front(){if (_head != nullptr){ListNode<T>* headNext = _head->_next;delete _head;_head = headNext;_size--;}}6.尾插法插入元素 push_back(T data)~
尾插法個人認為對于單鏈表的實際使用中,沒有任何意義(你真想要尾插,可以用雙鏈表啊)
void push_back(T data){if (_head == nullptr)//如果頭節點為空,則將此節點設為頭節點{_head = new ListNode<T>();_head->_data = data;_head->_next = nullptr;}else{ListNode<T>* curr = _head;for (; curr->_next != nullptr; curr = curr->_next);//找到尾結點,此時其next一定為空ListNode<T>* newNode = new ListNode<T>();curr->_next = newNode;newNode->_data = data;newNode->_next = nullptr;}_size++;}7.尾插法刪除元素 pop_back()~
同理,尾刪法,對于單鏈表,也是沒有意義的。但注意,真要寫的時候,需要兩個輔助的指針,才能很好地實現尾刪。
/*尾刪*/void pop_back(){if (_head != nullptr){ListNode<T>* curr = _head, * prev = _head;for (; curr->_next != nullptr; prev = curr, curr = curr->_next);//找到尾結點,需要額外的節點prev來作為curr的前驅,否則curr刪除后,其前驅的next指向的是野指針。delete curr;prev->_next = nullptr;curr = nullptr;_size--;}}8.按data刪除鏈表中其中一個元素 erase(T data)
基本也要兩個指針輔助刪除。
bool erase(T data)//刪除一個元素。{if (_head == nullptr){return false;}else if (_head->_data == data)//看頭節點是否是要被刪除的元素{ListNode<T>* headNext = _head->_next;delete _head;_head = headNext;_size--;return true;}for (ListNode<T>* prev = _head, *curr = _head->_next; curr != nullptr; prev = curr, curr = curr->_next){if (curr->_data == data){prev->_next = curr->_next;delete curr;curr = nullptr;_size--;return true;}}return false;}9.刪除符合data值的所有元素 remove(T data)
思路跟8差不多,就是需要對刪除的這個元素,是不是頭節點,進行區分。
理解的話,可以自行畫圖理解(比如 1->1->2刪除1)(1->2->2刪除2)
10.找到倒數第k個節點-解法1 Find_K_Node(const int& k)
由于在定義鏈表的時候,我們定義了_size這個成員變量,所以直接利用這個變量。
//找倒數第k個節點ListNode<T>* Find_K_Node(const int& k)const{if (k<1 || k>_size){return nullptr;}int number = 0;int length = _size - k + 1;//對應倒數第k個的正數。for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next){if (++number == length){return curr;}}return nullptr;//只有當_head=nullptr時,會出現這種情況。}11.找到倒數第k個節點-解法2 Find_K_Node2(const int& k)
此法為不借助size的解法,也是網上常規的,利用兩個指針的解法。
當第一個指針達到k-1個位置的時候,第二個指針開始出發,當第一個指針到達尾節點的時候,第二個指針此時的節點,就是倒數第k個節點。
//找倒數第k個節點//不借助sizeListNode<T>* Find_K_Node2(const int& k)const{if (k < 1 || _head == nullptr)//保證不越界,以及鏈表為空的情況{return nullptr;}int number = 0;ListNode<T>* KNode = nullptr, * curr = nullptr;for (curr = _head; curr->_next != nullptr; curr = curr->_next)//這里需要改成curr->next不能為空{++number;if (number == k - 1){KNode = _head;//當第一個指針指的位置離head有k-1個時,第二個指針就可以開始出發了。}else if (number > k - 1 && KNode != nullptr)//當第二個指針可以出發時{KNode = KNode->_next;}}if (k == 1)//如果是倒數第一個{return curr;}if (KNode != nullptr)//如果第二個指針不為空,則返回第二個指針。{return KNode;}return nullptr;//當_head=nullptr時,或者KNode為nullptr時,會出現這種情況。}12.反轉鏈表 reverse()
看似麻煩,實際上,畫個圖就能解決,首先反轉,必須要遍歷整個鏈表,在遍歷過程中完成反轉。因此,可以試著畫(1->2->3)進行反轉和(1->2->3->4)進行反轉。思路就會清晰很多。
在下列函數中,用到了3個指針,分別指向當前節點的前驅,當前節點,當前節點的后繼。以完成鏈表的反轉。
并要對頭節點進行額外的考慮。
//反轉鏈表void reverse()//畫圖1 2 3 4進行分析就能明白,需要3個指針進行輔助。{if (_head == nullptr){return;}ListNode<T>* curr = _head->_next, * prev = _head, * next = _head->_next;for (; curr != nullptr; prev = curr, curr = next){next = curr->_next;curr->_next = prev;if (prev == _head){prev->_next = nullptr;}}//循環完之后,此時curr為nullptr,prev為尾結點_head = prev;//就更新頭節點。只有一個節點時,這個也成立}13.正向輸出單鏈表 PrintList()
/*遍歷輸出*/void PrintList()const{for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next){cout << curr->_data << endl;}cout << "size 為" << _size << endl;}14.反向遍歷輸出單鏈表 PrintReverseList()
此時,只需要一個輔助棧,就能解決反向輸出的問題
//反向遍歷輸出void PrintReverseList()const{stack<T> DataStack;for (ListNode<T>* curr = _head; curr != nullptr; curr = curr->_next){DataStack.push(curr->_data);}while (!DataStack.empty()){cout << DataStack.top() << endl;DataStack.pop();}cout << "size 為" << _size << endl;}其中empty()函數為
bool empty()const{return !_size;}15.獲取鏈表的首元素 front()
這個會在下面的合并兩個排序的鏈表,以及找兩個鏈表公共的節點中用到。
實際上這個函數并不規范,在stl的list中,front()成員函數是用來得到節點的data(ValueType)值的。在此處為了方便起見,可以直接得到_head。(實際上,省事點,將_head設為public就行了)
//首元素//實際上這個不是很規范ListNode<T>*& front(){return _head;}16.得到鏈表的長度 getSize()
int getSize()const{return _size;}(三)非成員函數騷操作鏈表題
1.合并兩個排序的鏈表
思路就是歸并排序的歸并操作。懂歸并排序的歸并原理,這個也就懂了。
//合并兩個排序的鏈表,假設都是從小到大排列 template<typename T> void MergeSortedList(MyForward_List<T>& L1, MyForward_List<T>& L2, MyForward_List<T>& L) {ListNode<T>* L1Node = L1.front();ListNode<T>* L2Node = L2.front();while (L1Node != nullptr && L2Node != nullptr){if (L1Node->_data < L2Node->_data){L.push_front(L1Node->_data);L1Node = L1Node->_next;}else{L.push_front(L2Node->_data);L2Node = L2Node->_next;}}while (L1Node != nullptr){L.push_front(L1Node->_data);L1Node = L1Node->_next;}while (L2Node != nullptr){L.push_front(L2Node->_data);L2Node = L2Node->_next;}L.reverse();//反轉鏈表,因為是按頭插的,順序會改變。 }2.找兩個鏈表最近公共的節點解法1
一般,是這樣的騷鏈表,才會出現這種情況(這里用到了其他老兄的一張圖)
第一種利用輔助棧,進行解決,相對蠻力的方式。將兩個鏈表從頭到尾遍歷一遍,將結果存到兩個棧中,再從兩個棧頂分別彈出元素,第一個不相等的元素的前一個棧頂元素,就必然是兩個鏈表的最近公共節點。
原理方面應該沒問題,個人沒有測試這段代碼,應該是沒問題的,有問題可以下方留言。
template<typename T> ListNode<T>* FirstCommonNode1(MyForward_List<T>& L1, MyForward_List<T>& L2)//不寫測試代碼,太麻煩//棧的方式也太繁瑣,不用 {ListNode<T>* L1Node = L1.front();ListNode<T>* L2Node = L2.front();if (L1Node == nullptr || L2Node == nullptr){return nullptr;}stack<ListNode<T>*> L1Stack;stack<ListNode<T>*> L2Stack;//將L1和L2的data 數據存到stack中for (ListNode<T>* curr = L1Node; curr != nullptr; curr = curr->_next){L1Stack.push(curr);}for (ListNode<T>* curr = L2Node; curr != nullptr; curr = curr->_next){L2Stack.push(curr);}ListNode<T>* CommonNode = nullptr;while (L1Stack.top() == L2Stack.top())//當棧頂元素不一致時,退出循環{CommonNode = L1Stack.top();L1Stack.pop();L2Stack.pop();}return CommonNode; }3.找兩個鏈表最近公共的節點解法2
這里用到了這個老哥(https://www.cnblogs.com/kevinsharif/p/9216807.html)的第七個部分的內容的思想。即將鏈表長度長的鏈表先遍歷到跟短的鏈表一樣的長度,再一個個比較。第一個相同的節點,就必然是兩個鏈表的最近公共節點。
template<typename T> ListNode<T>* FirstCommonNode2(MyForward_List<T>& L1, MyForward_List<T>& L2)//不寫測試代碼,太麻煩//減長度的方式更高效 {ListNode<T>* L1Node = L1.front();ListNode<T>* L2Node = L2.front();if (L1Node == nullptr || L2Node == nullptr){return nullptr;}int L1Size = L1.getSize();int L2Size = L2.getSize();int diff = L1Size - L2Size;if (diff > 0)//選擇最長的那個,“剪去”多余的長度{for (ListNode<T>* curr = L1Node; diff > 0; diff--){curr = curr->_next;}}else{diff = -diff;for (ListNode<T>* curr = L2Node; diff > 0; diff--){curr = curr->_next;}}//到這一步,兩個鏈表的長度應該一致。for (ListNode<T>* curr = L1Node, *curr2 = L2Node; curr != nullptr; curr = curr->_next, curr2 = curr2->_next){if (curr == curr2)//只要碰到一個相等的,就返回。{return curr;}}return nullptr;//說明兩個鏈表沒有公共的節點 }(四)后序可能添加的
網上看到一些騷題,比如判斷一個單鏈表是否有環,環中節點個數,環入口等等題,有時間我再看看并進行總結,雖然其他人也總結的比較多也比較好了。
說實話,個人認為這些真心沒意義,除了面試或者考試只能考這些以外。= =。不過,如果你分析過二叉樹,紅黑樹等的寫法的話,再來看鏈表的這些騷操作題,哪怕跟我一樣第一次看,也能很快理解和掌握。然后為了面試再把這些背下來(o(╯□╰)o)。
對我寫的代碼不是很懂的,可以在下方留言,有必要的話,我再進行更多的注解。實際上對于這種指針題,畫圖就能解決大部分問題!剩下就看悟性和數學功底了。
總結
以上是生活随笔為你收集整理的c++ 数据结构和算法之刷无聊的面试题(1)-单链表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数字时代,互联网企业的组织形态
- 下一篇: c++ opencv 将视频转化成字符串