算法总结之编码(C++)
算法刷題:https://leetcode-cn.com/problemset/all/
1.把文件中的一組整數排序后輸出到另一個文件中
考點:運用vector容器解決實際問題
這個題目牽涉到文件操作以及排序。我們可以使用vector容器來簡化文件操作。在讀文件的時候用push_back把所有的整數放入一個vector<int>對象中,在寫文件時用[]操作符直接把vector<int>對象輸出到文件。代碼如下:
2.已知單向鏈表的頭結點head,寫一個函數把這個鏈表逆序
我們假設單向鏈表的節點如下:typedef struct LIST_NODE {int data;LIST_NODE *next; }LIST_NODE;方法一:迭代循環,不使用額外的節點存儲空間 LINK_NODE *ReverseLink(LINK_NODE *head) {LINK_NODE *next;LINK_NODE *prev = NULL; //循環的初始條件是:prev = NULL;while(head != NULL) //循環終止條件是:head == NULL{next = head->next; //保存下一個節點head->next = prev; prev = head;head = next; //頭指針往前移}return prev; //返回新鏈表頭節點 }方法二:遞歸,終止條件就是鏈表只剩一個節點時直接返回這個節點的指針 LINK_NODE *ReverseLink(LINK_NODE *head) {LINK_NODE *newHead;if((head == NULL) || (head->next == NULL))return head;newHead = ReverseLink(head->next); /*遞歸部分*/head->next->next = head; /*核心:回朔部分*/head->next = NULL;return newHead; }對于線性數據結構,比較適合用迭代循環方法,而對于樹狀數據結構,比如二叉樹,遞歸方法則非常簡潔優雅。
(2) 已知兩個鏈表 head1 和 head2 各自有序,請把它們合并成一個鏈表依然有序。 ( 保留所有結點,即便大小相同) Node * Merge(Node *head1 , Node *head2) {if ( head1 == nullptr)return head2;if ( head2 == nullptr)return head1 ;Node *head = nullptr;Node *p1 = nullptr;Node *p2 = nullptr;if ( head1->data < head2->data ){head = head1;p1 = head1->next;p2 = head2;}else{head = head2;p2 = head2->next;p1 = head1;}Node *pCurrent = head;while ( p1 != nullptr && p2 != nullptr){if ( p1->data <= p2->data ){pCurrent->next = p1;pCurrent = p1;p1 = p1->next;}else{pCurrent->next = p2;pCurrent = p2;p2 = p2->next;}}if ( p1 != nullptr )pCurrent->next = p1;if ( p2 != nullptr )pCurrent->next = p2;return head; } (3) 已知兩個鏈表 head1 和 head2 各自有序,請把它們合并成一個鏈表依然有序,這次要求用遞歸方法進行。 Node * MergeRecursive(Node *head1 , Node *head2) {if ( head1 == nullptr )return head2;if ( head2 == nullptr)return head1;Node *head = nullptr;if ( head1->data < head2->data ){head = head1;head->next = MergeRecursive(head1->next,head2);}else{head = head2;head->next = MergeRecursive(head1,head2->next);}return head; }3.寫一個函數找出一個整數數組中,第二大的數
const int MINNUMBER = -32767; int find_sec_max( int data[] , int count) {int maxnumber = data[0]; //初始化第一大數int sec_max = MINNUMBER; //初始化第二大數for ( int i = 1 ; i < count ; i++){if ( data[i] > maxnumber ) //是否大于最大數,修改第一大數和第二大數{sec_max = maxnumber;maxnumber = data[i];}else{if ( data[i] > sec_max ) //是否大于第二大數sec_max = data[i];}}return sec_max; }4.如何判斷一個單鏈表是否有環的
基本思路:用一個慢指針,一個快指針,慢指針一次走一個節點,快指針一次都兩個節點,判斷快指針是否能追上慢指針,當快指針追上慢指針時,說明有環,否則無環
bool check(const NODE* head) {if(head == NULL) return false;NODE *low =head;NODE *fast=head->next;while (fast != NULL && fast->next != NULL){low = low->next;fast = fast->next->next;if(low == fast) return true;}return false; }5.如果編寫一個標準 strcpy 函數
char* strcpy(char *strDest, const char *strSrc) // 輸入參數 const {assert( (strDest != NULL) &&(strSrc != NULL) ); //斷言char *address = strDest; while( (*strDest++ = * strSrc++) != ‘/0’ );return address; //支持鏈式操作 }int strlen( const char *str ) // 輸入參數 const {assert( strt != NULL ); // 斷言字符串地址非 0int len=0; // 注,一定要初始化。 while( (*str++) != '/0' ){len++;}return len; }6.String?的具體實現
已知 String 類定義如下: class String { public:String(const char *str = NULL); // 通用構造函數 String(const String &another); // 拷貝構造函數 ~ String(); // 析構函數 String & operater =(const String &rhs); // 賦值函數 private:char *m_data; // 用于保存字符串 };寫出類的成員函數實現: String::String(const char *str) {if ( str == NULL ) //strlen 在參數為 NULL 時會拋異常才會有這步判斷 {m_data = new char[1] ;m_data[0] = '/0' ;}else{m_data = new char[strlen(str) + 1];strcpy(m_data, str);} } String::String(const String &another) {m_data = new char[strlen(another.m_data) + 1];strcpy(m_data, other.m_data); }String& String::operator =(const String &rhs) {if ( this == &rhs) //自拷貝return *this ;delete []m_data; // 刪除原來的數據,新開一塊內存 m_data = new char[strlen(rhs.m_data) + 1];strcpy(m_data,rhs.m_data);return *this ; }String::~String() {delete []m_data ; }??7.用遞歸算法判斷數組 a[N] 是否為一個遞增數組
遞歸的方法,記錄當前最大的,并且判斷當前的是否比這個還大,大則繼續,否則返回 false 結束:bool fun( int a[], int n ) {if( n==1 )return true;if( n==2 )return a[n-1] >= a[n-2];return fun(a, n-1) && ( a[n-1] >= a[n-2] ); }8.判斷字符串是否為回文
#include <stdio.h> #include <string.h>int main() {char a[100]= {0};int i = 0;int len = 0;printf("please input character string:\n");gets(a);len = strlen(a); //計算輸入字符串的長度;for(i = 0; i < (len / 2); i++) //只需要判斷前一半(len/2)長度就好了{ if(a[i] != a[len - 1 - i]) //判斷是否為回文數;{printf("不是回文數\n");return 0;}}printf("是回文數\n");return 0; }8.字符串倒序
寫一個函數將 "tom is cat" 倒序打印出來,即 "cat is tom"
思路1:按空格分隔單詞,保存到std::vector<string>,最后逆序輸出單詞
思路2:逆序字符串,逐個單詞判斷,遇到空格,輸出一個單詞,需要記住每個單詞的結尾和開頭
9.請判斷兩個字符是否互為變形詞
給定兩個字符串str1與str2,如果兩個字符串中出現的字符種類一樣且每種字符出現的次數也一樣,那么str1與str2互為變形詞,
分析:使用哈希表分別記錄每個字符出現的次數。也可以建立一個256的數組,代替哈希表。bool simpleWord(char *str1, char *str2, const int &length) {int num1[256] = { 0 };int num2[256] = { 0 };for (int i = 0; i < length; ++i) {++num1[str1[i]];}for (int i = 0; i < length; ++i) {++num2[str2[i]];}bool flag = true;for (int i = 0; i < length; ++i) {if (num1[i] != num2[i]) {flag = false;break;}}return flag; }10.i代表str中的位置,將str[0…i]移到右側,str[i+1…N-1]移到左側
要求:時間復雜度為O(N) O(N)O(N),額外空間復雜度為O(1) O(1)O(1)。
舉例:str=“ABCDE”,i=2,將str調整為“CDEAB”
可以直接用substr!
分析: 1.首先將0-i逆序 2.將i+1-N-1逆序 3.將str整體逆序#include<iostream> #include<string> #include<vector>void reverseWord(std::string &str) { //字符串反向for (int i = 0; i < str.length() / 2; ++i) {auto temp = str[i];str[i] = str[str.length() - i - 1];str[str.length() - i - 1] = temp;} }void reverseWord(std::string &str, const int &start,const int &end) { //字符串從start到end 反轉for (int i = 0; i < (end - start + 1) / 2; ++i) {char temp = str[start + i];str[start + i] = str[end - i];str[end - i] = temp;} }void translationWord(std::string &str, const int &position) { //將字符串0~position移到最右側reverseWord(str, 0, position); //"ABCDE"變"BACDE"reverseWord(str, position + 1, str.length() - 1); //"BACDE"變"BAEDC"reverseWord(str); //"BAEDC"變"CDEAB" }11.給定一個字符串str,將其中所有空格字符替換成“%20”,假設str后面有足夠的空間。
分析: 1.計算出空格數量,得到最終字符的長度。 2.從后往前寫入字符。#include<string>void replaceSpace(std::string &str) {//將空格替換為“%20”int spaceNum = 0;int lenOld = str.length() - 1;for (auto i : str) {if (i == ' ') {++spaceNum;}}std::string addition(spaceNum * 2, ' ');str = str + addition;int lenNew = lenOld + 2 * spaceNum;for (int i = lenOld; i >= 0; --i) {if (str[i] != ' ') {str[lenNew--] = str[i];}else if (str[i] == ' ') {str[lenNew--] = '0';str[lenNew--] = '2';str[lenNew--] = '%';}} }12.給定一個字符串,其中只包含左括號或右括號,判斷是不是整體有效的括號字符串。
時間復雜度為O(N) O(N)O(N),額外空間復雜度為O(1) O(1)O(1)。
分析:使用num計算左右括號數,如果是左括號num++,如果是右括號num––,遍歷的過程中如果出現num<0,則直接返回false,最終如果num==0,則返回true,否則為false。
13.給定一個字符串str,返回str的最長無重復字符子串的長度
蠻力法自然是兩層for循環搞定,但是效率堪憂,需要想辦法將復雜度調整到O(n),也就是只遍歷一遍字符串,這就需要記錄在之前是否有遇到某個字符,可以使用unordered_map,不過這里因為僅僅是字符,那么創建一個大小為256的vector就可以了,反而顯得節省空間。 細想,每個不包括重復字符的子串肯定是一段連續的字符區域,頭設定為front,尾設定為back。那么在僅僅遍歷一遍的情況下,front肯定是不斷改變了,back則就是當前遍歷到的位置如果當前遍歷到的這個字符在front后面沒有出現過,那么front不需要移動,接著遍歷后面的字符 如果當前遍歷到的這個字符在front后面出現過,那么從front到當前位置這個子串肯定就有重復的字符了,此時就需要改變front的位置到出現的那個字符后面的位置。也就是和當前遍歷到的這個字符上一次出現的位置的下一個位置。 在這個過程中,時刻更新最大的長度,因為front到back這段區域永遠不可能有重復的字符,如果有,已經在第二步解決了class Solution { public:int lengthOfLongestSubstring(string s) {/* 初始256大小,因為所有字符也就這么多,值記錄著最后一次出現時在s中的下標,沒有則是-1 */vector<int> dp(256, -1);int max_len = 0;int front = 0;for(int i = 0; i < s.size(); ++i){/* 如果當前字符在front后面出現過一次 */if(dp[s[i]] != -1 && dp[s[i]] >= front){/* 改變front的位置,指向當前遍歷到的字符上一次出現的位置的后面的位置 */front = dp[s[i]] + 1;}/* 時刻更新每個字符最后一次出現時的位置 */dp[s[i]] = i;/* 時刻計算最大的長度,當前子串區域為[front, i] */max_len = max(max_len, i - front + 1);}return max_len;}};(2) 求最大不重復子串,在最后求長度地方修改下即可: if (i - front + 1 > max_len) {max_len = max(max_len, i - front + 1);memcpy(maxSubString, &s[front], max_len); }14.象搜索的輸入信息是一個字符串,統計300萬輸入信息中的最熱門的前10條,我們每次輸入的一個字符串為不超過255byte,內存使用只有1G。請描述思想,寫出算法(c語言),空間和時間復雜度。
答案:?
300萬個字符串最多(假設沒有重復,都是最大長度)占用內存3M*1K/4=0.75G。所以可以將所有字符串都存放在內存中進行處理。?
可以使用key為字符串,值為字符串出現次數的hash來統計每個每個字符串出現的次數。并用一個長度為10的數組/鏈表來存儲目前出現次數最多的10個字符串。?這樣空間和時間的復雜度都是O(n)。
https://blog.csdn.net/moonboat0331/article/details/14000731
15.海量數據處理 - 10億個數中找出最大的10000個數(top K問題)
https://blog.csdn.net/zyq522376829/article/details/47686867
16.最長回文子串(C語言)
https://blog.csdn.net/aixiaodeshushu/article/details/83690213
17.海量數據排序——如果有1TB的數據需要排序,但只有32GB的內存如何排序處理?
?1、外排序?
傳統的排序算法一般指內排序算法,針對的是數據可以一次全部載入內存中的情況。但是面對海量數據,即數據不可能一次全部載入內存,需要用到外排序的方法。外排序采用分塊的方法(分而治之),首先將數據分塊,對塊內數據按選擇一種高效的內排序策略進行排序。然后采用歸并排序的思想對于所有的塊進行排序,得到所有數據的一個有序序列。
例如,考慮一個1G文件,可用內存100M的排序方法。首先將文件分成10個100M,并依次載入內存中進行排序,最后結果存入硬盤。得到的是10個分別排序的文件。接著從每個文件載入9M的數據到輸入緩存區,輸出緩存區大小為10M。對輸入緩存區的數據進行歸并排序,輸出緩存區寫滿之后寫在硬盤上,緩存區清空繼續寫接下來的數據。對于輸入緩存區,當一個塊的9M數據全部使用完,載入該塊接下來的9M數據,一直到所有的9個塊的所有數據都已經被載入到內存中被處理過。最后我們得到的是一個1G的排序好的存在硬盤上的文件。
2、1TB數據使用32GB內存如何排序?
?、佟汛疟P上的1TB數據分割為40塊(chunks),每份25GB。(注意,要留一些系統空間!)?
?、?、順序將每份25GB數據讀入內存,使用quick sort算法排序。?
?、邸雅判蚝玫臄祿?#xff08;也是25GB)存放回磁盤。?
?、?、循環40次,現在,所有的40個塊都已經各自排序了。(剩下的工作就是如何把它們合并排序!)?
?、荨?0個塊中分別讀取25G/40=0.625G入內存(40 input buffers)。?
?、?、執行40路合并,并將合并結果臨時存儲于2GB 基于內存的輸出緩沖區中。當緩沖區寫滿2GB時,寫入硬盤上最終文件,并清空輸出緩沖區;當40個輸入緩沖區中任何一個處理完畢時,寫入該緩沖區所對應的塊中的下一個0.625GB,直到全部處理完成。
3、繼續優化?
磁盤I/O通常是越少越好(最好完全沒有),那么如何降低磁盤I/O操作呢?關鍵就在第5和第6步中的40路輸入緩沖區,我們可以先做8路merge sort,把每8個塊合并為1路,然后再做5-to-1的合并操作。?
再深入思考一下,如果有多余的硬件,如何繼續優化呢?有三個方向可以考慮:?
使用并發:如多磁盤(并發I/O提高)、多線程、使用異步I/O、使用多臺主機集群計算。?
提升硬件性能:如更大內存、更高RPM的磁盤、升級為SSD、Flash、使用更多核的CPU。?
提高軟件性能:比如采用radix sort、壓縮文件(提高I/O效率)等。
=================================================更多鏈表面試題=====================================================
1. 在 O(1) 時間刪除鏈表節點
題目描述:給定單向鏈表的頭指針和一個節點指針,定義一個函數在O(1)時間刪除該節點。
解題思路:常規的做法是從鏈表的頭結點開始遍歷,找到需要刪除的節點的前驅節點,把它的 next 指向要刪除節點的下一個節點,平均時間復雜度為O(n),不滿足題目要求。 那是不是一定要得到被刪除的節點的前一個節點呢?其實不用的。我們可以很方面地得到要刪除節點的下一個節點,如果我們把下一個節點的內容復制到要刪除的節點上覆蓋原有的內容,再把下一個節點刪除,那就相當于把當前要刪除的節點刪除了。舉個栗子,我們要刪除的節點i,先把i的下一個節點j的內容復制到i,然后把i的指針指向節點j的下一個節點。此時再刪除節點j,其效果剛好是把節點i給刪除了。 要注意兩種情況:
參考代碼
public static ListNode deleteNode(ListNode head, ListNode toBeDeleted) { // 如果輸入參數有空值就返回表頭結點 if (head == null || toBeDeleted == null) { return head; } // 如果刪除的是頭結點,直接返回頭結點的下一個結點 if (head == toBeDeleted) { return head.next; } // 下面的情況鏈表至少有兩個結點 // 在多個節點的情況下,如果刪除的是最后一個元素 if (toBeDeleted.next == null) { // 找待刪除元素的前驅 ListNode tmp = head; while (tmp.next != toBeDeleted) { tmp = tmp.next; } // 刪除待結點 tmp.next = null; } // 在多個節點的情況下,如果刪除的是某個中間結點 else { // 將下一個結點的值輸入當前待刪除的結點 toBeDeleted.value = toBeDeleted.next.value; // 待刪除的結點的下一個指向原先待刪除引號的下下個結點,即將待刪除的下一個結點刪除 toBeDeleted.next = toBeDeleted.next.next; } // 返回刪除節點后的鏈表頭結點 return head; }2. 翻轉單鏈表
題目描述:輸出一個單鏈表的逆序反轉后的鏈表。
解題思路:用三個臨時指針 prev、cur、next 在鏈表上循環一遍即可。
?
/* public class ListNode {int val;ListNode next = null;ListNode(int val) {this.val = val;} }*/ public class Solution {public ListNode ReverseList(ListNode head) {if(head == null)return null;ListNode pre = null;ListNode next = null;while(head != null){next = head.next;head.next = pre;pre = head;head = next;}return pre;} }3. 翻轉部分單鏈表:
題目描述:給定一個單向鏈表的頭結點head,以及兩個整數from和to,在單鏈表上把第from個節點和第to個節點這一部分進行反轉
舉例:1->2->3->4->5->null, from = 2, to = 4 結果:1->4->3->2->5->null
public ListNode reverseBetween(ListNode head, int m, int n) {if (head == null) return null;if (head.next == null) return head;int i = 1;ListNode reversedNewHead = null;// 反轉部分鏈表反轉后的頭結點ListNode reversedTail = null;// 反轉部分鏈表反轉后的尾結點ListNode oldHead = head;// 原鏈表的頭結點ListNode reversePreNode = null;// 反轉部分鏈表反轉前其頭結點的前一個結點ListNode reverseNextNode = null;while (head != null) {if (i > n) {break;}if (i == m - 1) {reversePreNode = head;}if (i >= m && i <= n) {if (i == m) {reversedTail = head;}reverseNextNode = head.next;head.next = reversedNewHead;reversedNewHead = head;head = reverseNextNode;} else {head = head.next;}i++;}reversedTail.next = reverseNextNode;if (reversePreNode != null) {reversePreNode.next = reversedNewHead;return oldHead;} else {return reversedNewHead;} } 復制代碼4. 旋轉單鏈表
題目描述:給定一個單鏈表,設計一個算法實現鏈表向右旋轉 K 個位置。 舉例: 給定 1->2->3->4->5->6->NULL, K=3 則4->5->6->1->2->3->NULL 解題思路:
- 方法一 雙指針,快指針先走k步,然后兩個指針一起走,當快指針走到末尾時,慢指針的下一個位置是新的順序的頭結點,這樣就可以旋轉鏈表了。
- 方法二 先遍歷整個鏈表獲得鏈表長度n,然后此時把鏈表頭和尾鏈接起來,在往后走n - k % n個節點就到達新鏈表的頭結點前一個點,這時斷開鏈表即可。
方法二代碼:
public class Solution { {public ListNode rotateRight(ListNode head, int k) {if (!head) return null;int n = 1;ListNode cur = head;while (cur.next) {++n;cur = cur.next;}cur.next = head;int m = n - k % n;for (int i = 0; i < m; ++i) {cur = cur.next;}ListNode newhead = cur.next;cur.next = NULL;return newhead;} }; 復制代碼5. 刪除單鏈表倒數第 n 個節點
題目描述:刪除單鏈表倒數第 n 個節點,1 <= n <= length,盡量在一次遍歷中完成。 解題思路:雙指針法,找到倒數第 n+1 個節點,將它的 next 指向倒數第 n-1個節點。
解題思路
經典的雙指針法。定義兩個指針,第一個指針從鏈表的頭指針開始遍歷向前走k-1步,第二個指針保持不動,從第k步開始,第二個指針也開始從鏈表的頭指針開始遍歷,由于兩個指針的距離保持在k-1,當第一個指針到達鏈表的尾節點時,第二個指針剛好指向倒數第k個節點。
關注要點
鏈表頭指針是否為空,若為空則直接返回回null
k是否為0,k為0也就是要查找倒數第0個節點,由于計數一般是從1開始的,所有輸入0沒有實際意義,返回null
k是否超出鏈表的長度,如果鏈表的節點個數少于k,則在指針后移的過程中會出現next指向空指針的錯誤,所以程序中要加一個判斷
?
6. 求單鏈表的中間節點
題目描述:求單鏈表的中間節點,如果鏈表的長度為偶數,返回中間兩個節點的任意一個,若為奇數,則返回中間節點。 解題思路:快慢指針,慢的走一步,快的走兩步,當快指針到達尾節點時,慢指針移動到中間節點。
// 遍歷一次,找出單鏈表的中間節點 public ListNode findMiddleNode(ListNode head) {if (nullptr == head) {return;}ListNode slow = head;ListNode fast = head;while (nullptr != fast && nullptr != fast.next) {fast = fast.next.next;slow = slow.next;}return slow; }7. 鏈表劃分
題目描述: 給定一個單鏈表和數值x,劃分鏈表使得所有小于x的節點排在大于等于x的節點之前。
public class Solution {/*** @param head: The first node of linked list.* @param x: an integer* @return: a ListNode */public ListNode partition(ListNode head, int x) {// write your code hereif(head == null) return null;ListNode leftDummy = new ListNode(0);ListNode rightDummy = new ListNode(0);ListNode left = leftDummy, right = rightDummy;while (head != null) {if (head.val < x) {left.next = head;left = head;} else {right.next = head;right = head;}head = head.next;}right.next = null;left.next = rightDummy.next;return leftDummy.next;} }8. 鏈表求和
題目描述:你有兩個用鏈表代表的整數,其中每個節點包含一個數字。數字存儲按照在原來整數中相反的順序,使得第一個數字位于鏈表的開頭。寫出一個函數將兩個整數相加,用鏈表形式返回和。 解題思路:做個大循環,對每一位進行操作:
當前位:(A[i]+B[i])%10 進位:(A[i]+B[i])/10
public class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {ListNode c1 = l1;ListNode c2 = l2;ListNode sentinel = new ListNode(0);ListNode d = sentinel;int sum = 0;while (c1 != null || c2 != null) {sum /= 10;if (c1 != null) {sum += c1.val;c1 = c1.next;}if (c2 != null) {sum += c2.val;c2 = c2.next;}d.next = new ListNode(sum % 10);d = d.next;}if (sum / 10 == 1)d.next = new ListNode(1);return sentinel.next;} }9. 單鏈表排序
題目描述:在O(nlogn)時間內對鏈表進行排序。 快速排序:
public ListNode sortList(ListNode head) {//采用快速排序quickSort(head, null);return head; } public static void quickSort(ListNode head, ListNode end) {if (head != end) {ListNode node = partion(head, end);quickSort(head, node);quickSort(node.next, end);} }public static ListNode partion(ListNode head, ListNode end) {ListNode p1 = head, p2 = head.next;//走到末尾才停while (p2 != end) {//大于key值時,p1向前走一步,交換p1與p2的值if (p2.val < head.val) {p1 = p1.next;int temp = p1.val;p1.val = p2.val;p2.val = temp;}p2 = p2.next;}//當有序時,不交換p1和key值if (p1 != head) {int temp = p1.val;p1.val = head.val;head.val = temp;}return p1; }歸并排序:
public ListNode sortList(ListNode head) {//采用歸并排序if (head == null || head.next == null) {return head;}//獲取中間結點ListNode mid = getMid(head);ListNode right = mid.next;mid.next = null;//合并return mergeSort(sortList(head), sortList(right)); }/*** 獲取鏈表的中間結點,偶數時取中間第一個** @param head* @return*/ private ListNode getMid(ListNode head) {if (head == null || head.next == null) {return head;}//快慢指針ListNode slow = head, quick = head;//快2步,慢一步while (quick.next != null && quick.next.next != null) {slow = slow.next;quick = quick.next.next;}return slow; }/**** 歸并兩個有序的鏈表** @param head1* @param head2* @return*/ private ListNode mergeSort(ListNode head1, ListNode head2) {ListNode p1 = head1, p2 = head2, head;//得到頭節點的指向if (head1.val < head2.val) {head = head1;p1 = p1.next;} else {head = head2;p2 = p2.next;}ListNode p = head;//比較鏈表中的值while (p1 != null && p2 != null) {if (p1.val <= p2.val) {p.next = p1;p1 = p1.next;p = p.next;} else {p.next = p2;p2 = p2.next;p = p.next;}}//第二條鏈表空了if (p1 != null) {p.next = p1;}//第一條鏈表空了if (p2 != null) {p.next = p2;}return head; } 復制代碼10. 合并兩個排序的鏈表
題目描述:輸入兩個單調遞增的鏈表,輸出兩個鏈表合成后的鏈表,當然我們需要合成后的鏈表滿足單調不減規則。
ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {if(pHead1 == nullptr)return pHead2;else if(pHead2 == nullptr)return pHead1;ListNode* pMergedHead = nullptr;if(pHead1->value < pHead2->value){pMergedHead = pHead1;pMergedHead->next = Merge(pHead1->next, pHead2);}else{pMergedHead = pHead2;pMergedHead->next = Merge(pHead1, pHead2->next);}return pMergedHead; }11. 復雜鏈表的復制
題目描述:輸入一個復雜鏈表(每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點),返回結果為復制后復雜鏈表的head。(注意,輸出結果中請不要返回參數中的節點引用,否則判題程序會直接返回空)
[劍指offer] 復雜鏈表的復制
12. 刪除鏈表中重復的結點
題目描述:在一個排序的鏈表中,存在重復的結點,請刪除該鏈表中重復的結點,重復的結點不保留,返回鏈表頭指針。 例如,鏈表1->2->3->3->4->4->5 處理后為 1->2->5
?
###方法1(粗暴)
我們采用最容易想到的方法,不用考慮時間效率與空間效率問題,就直接用遍歷,用一個哈希表結構來記錄每個節點對應的值在節點中出現了幾次,統計完成后,然后通過哈希表中如果出現次數為大于等于2,再用兩個指針一個前一個后開始找對應的值就行了,如果后一個指針指向的節點的值為哈希表中出現次數大于等于2的數,那么前一個指針指向后一個指針的next然后刪除后一個指針所指向的節點,讓后一個指針重新指向前一個指針的next,就這樣就可以完成刪除。
雖然這個方法可以,但是時間復雜度為O(N), 空間復雜度為一個哈希表的結構,所以采用這種不是最佳選擇。
###方法2(三指針法)
采用三個指針來進行遍歷,同時刪除重復的節點,因為是有序的鏈表,我們就可以確定,重復的元素肯定是在一塊鏈接,所以我們就可以,用三指針,我們這里就叫
pre、cur、nex 分別代表的是前中后三個指針,我們在考慮的情況中,如果頭節點開始就重復,我們就處理很起來多了一種情況就需要額外處理,所以我們添加一個頭節點,變成帶頭節點,保證了頭節點開始不會重復,那么我們就可以開是讓pre指向帶頭的節點,cur指向pre的next,nex指向cur的next。
接下來我們就可以看cur是否和nex相等,相等就讓nex繼續向下走,不相等然后再處理刪除,cur開始到nex中間節點都是要刪除的(包含cur指向,不包含nex指向)刪除,就用到了pre,刪除完成讓pre指向cur就可以了。
如果cur值與nex值不相等,那么就可以三個指針各自往前移動一個。
下來我來看代碼~
ListNode* deleteDuplication(ListNode* pHead) {// 先判斷空if (pHead == NULL){return NULL;}// 判斷是否只有一個節點if (pHead->next == NULL){return pHead;}// 我們采用帶頭鏈表,自己添加一個頭ListNode* pre = new ListNode();pre->next = pHead; // 把頭節點鏈接在鏈表上ListNode* pre_head = pre; // 用來保存頭節點,用于返回刪除后的鏈表ListNode* cur = pHead; //中指針ListNode* nex = pHead->next; // 后面指針while (nex != nullptr) // 結束條件while (nex != nullptr && cur->val == nex->val)?{nex = nex->next;}// 如果沒有重復的那么cur的next一定等于nexif (cur->next != nex) // 如果相等說明沒有相同的節點{while (cur != nex) // 刪除動作{pre->next = cur->next;delete cur;cur = pre->next;}if (nex != nullptr) // 這里一定要要注意,要防止走到NULL發生段錯誤nex = nex->next;}else{// 處理沒有重復的情況pre = cur;nex = nex->next;cur = cur->next;}}ListNode* head = pre_head->next; // 釋放空間,防止內存泄漏delete pre_head;return head; }我們測試要盡可能的全面
1)傳NULL
2)只有一個節點
3)頭節點開始就有重復
4)中間節點重復
5)尾部節點重復
6)鏈表中沒有重復鏈表
7)所有節點都是重復的
?
13. 判斷單鏈表是否存在環
題目描述:判斷一個單鏈表是否有環 分析:快慢指針,慢指針每次移動一步,快指針每次移動兩步,如果存在環,那么兩個指針一定會在環內相遇。
14. 單鏈表是否有環擴展:找到環的入口點
題目描述:判斷單鏈表是否有環,如果有,找到環的入口點
解法1:
新建一個HashSet,遍歷鏈表,每次嘗試添加當前遍歷的節點到HashSet,如果添加失敗,則代表HashSet中已經包含該節點,此時的節點即為環的入口點.
代碼如下:
ListNode* solution1(ListNode *pHead) {ListNode* node = pHead;unordered_set<ListNode*> set;while (node != null) {if (!set.add(node)) {return node;}node = node->next;}return nullptr;}因為額外申請了內存,所以空間復雜度為O(n),時間復雜度為O(n)
解法2:
代碼如下:
ListNode* solution2(ListNode* pHead) {ListNode* fast = pHead;ListNode* slow = pHead;while (fast != nullptr && fast->next != nullptr) {fast = fast->next->next;slow = slow->next;if (fast == slow) {fast = pHead;while (slow != fast) {//這部分沒有理解fast = fast->next;slow = slow->next;}return slow;}}return nullptr;}此處并沒有嚴格按照1,2,3的步驟,因為第一次兩個指針相遇的節點處,就是3中先走的n步,假設slow指針走了x步,那么第一次相遇的時候,fast走了2x步,如果環中有n個節點,則fast比slow多走了n步,即2x-n=x,則x=n,所以兩個指針第一次相遇的節點就是一個指針從鏈表頭走了環中節點個數n步的位置.
這里并沒有額外申請空間,只是用了兩個指針變量,所以空間復雜度為O(1),時間復雜度為O(n)
15. 判斷兩個無環單鏈表是否相交
題目描述:給出兩個無環單鏈表 解題思路:
- 方法一 最直接的方法是判斷 A 鏈表的每個節點是否在 B 鏈表中,但是這種方法的時間復雜度為 O(Length(A) * Length(B))。
- 方法二 轉化為環的問題。把 B 鏈表接在 A 鏈表后面,如果得到的鏈表有環,則說明兩個鏈表相交。可以之前討論過的快慢指針來判斷是否有環,但是這里還有更簡單的方法。如果 B 鏈表和 A 鏈表相交,把 B 鏈表接在 A 鏈表后面時,B 鏈表的所有節點都在環內,所以此時只需要遍歷 B 鏈表,看是否會回到起點就可以判斷是否相交。這個方法需要先遍歷一次 A 鏈表,找到尾節點,然后還要遍歷一次 B 鏈表,判斷是否形成環,時間復雜度為 O(Length(A) + Length(B))。
- 方法三 除了轉化為環的問題,還可以利用“如果兩個鏈表相交于某一節點,那么之后的節點都是共有的”這個特點,如果兩個鏈表相交,那么最后一個節點一定是共有的。所以可以得出另外一種解法,先遍歷 A 鏈表,記住尾節點,然后遍歷 B 鏈表,比較兩個鏈表的尾節點,如果相同則相交,不同則不相交。時間復雜度為 O(Length(A) + Length(B)),空間復雜度為 O(1),思路比解法 2 更簡單。
方法三的代碼:
boolean isIntersect(ListNode *headA, ListNode *headB) {if (nullptr == headA || nullptr == headB) {return false;}if (headA == headB) {return true;}while (nullptr != headA->next) {headA = headA->next;}while (nullptr != headB->next) {headB = headB->next;}return headA == headB; }16. 兩個鏈表相交擴展:求兩個無環單鏈表的第一個相交點
題目描述:找到兩個無環單鏈表第一個相交點,如果不相交返回空,要求在線性時間復雜度和常量空間復雜度內完成。 解題思路:
- 方法一 如果兩個鏈表存在公共結點,那么它們從公共結點開始一直到鏈表的結尾都是一樣的,因此我們只需要從鏈表的結尾開始,往前搜索,找到最后一個相同的結點即可。但是題目給出的單向鏈表,我們只能從前向后搜索,這時,我們就可以借助棧來完成。先把兩個鏈表依次裝到兩個棧中,然后比較兩個棧的棧頂結點是否相同,如果相同則出棧,如果不同,那最后相同的結點就是我們要的返回值。
- 方法二 先找出2個鏈表的長度,然后讓長的先走兩個鏈表的長度差,然后再一起走,直到找到第一個公共結點。
- 方法三 由于2個鏈表都沒有環,我們可以把第二個鏈表接在第一個鏈表后面,這樣就把問題轉化為求環的入口節點問題。
- 方法四 兩個指針p1和p2分別指向鏈表A和鏈表B,它們同時向前走,當走到尾節點時,轉向另一個鏈表,比如p1走到鏈表 A 的尾節點時,下一步就走到鏈表B,p2走到鏈表 B 的尾節點時,下一步就走到鏈表 A,當p1==p2 時,就是鏈表的相交點
方法四的代碼:
ListNode* getIntersectionNode(ListNode *headA, ListNode *headB) {if (nullptr == headA || nullptr == headB) {return null;}if (headA == headB) {return headA;}ListNode p1 = headA;ListNode p2 = headB;while (p1 != p2) {// 遍歷完所在鏈表后從另外一個鏈表再開始// 當 p1 和 p2 都換到另一個鏈表時,它們對齊了:// (1)如果鏈表相交,p1 == p2 時為第一個相交點// (2)如果鏈表不相交,p1 和 p2 同時移動到末尾,p1 = p2 = nullptr,然后退出循環p1 = (nullptr == p1) ? headB : p1->next;p2 = (nullptr == p2) ? headA : p2->next;}return p1; }17. 兩個鏈表相交擴展:判斷兩個有環單鏈表是否相交
題目描述:上面的問題是針對無環鏈表的,如果是鏈表有環呢? 解題思路:如果兩個有環單鏈表相交,那么它們一定共有一個環,即環上的任意一個節點都存在于兩個鏈表上。因此可以先用之前快慢指針的方式找到兩個鏈表中位于環內的兩個節點,如果相交的話,兩個節點在一個環內,那么移動其中一個節點,在一次循環內肯定可以與另外一個節點相遇。
如果兩個鏈表無環,判斷是否相交很簡單,判斷兩個環的最后一個節點指針是否相等即可。
分析:如果有環且兩個鏈表相交,則兩個鏈表都有共同一個環,即環上的任意一個節點都存在于兩個鏈表上。因此,就可以判斷一鏈表上倆指針相遇的那個節點,在不在另一條鏈表上。
無環鏈表和有環鏈表是不可能相交的;
兩個有環鏈表若相交,其“整個環上”的所有node一定都重合;
有環鏈表的相交,情況只有2種:相交于”不是環的部分”或相交于”環上“,即下圖所示;
?
//判斷單鏈表是否存在環,參數circleNode是環內節點,后面的題目會用到 bool hasCircle(Node *head,Node *&circleNode) {Node *slow,*fast;slow = fast = head;while(fast != NULL && fast->next != NULL){fast = fast->next->next;slow = slow->next;if(fast == slow){circleNode = fast;return true;}}return false; } //判斷兩個帶環鏈表是否相交 bool isIntersectWithLoop(Node *h1,Node *h2) {Node *circleNode1,*circleNode2;if(!hasCircle(h1,circleNode1)) ? ?//判斷鏈表帶不帶環,并保存環內節點return false; ? ? ? ? ? ? ? ?//不帶環,異常退出if(!hasCircle(h2,circleNode2))return false;Node *temp = circleNode2->next;while(temp != circleNode2){if(temp == circleNode1)return true;temp = temp->next;}return false; }18.孩子們的游戲(圓圈中最后剩下的數)
描述
每年六一兒童節,??投紩蕚湟恍┬《Y物去看望孤兒院的小朋友,今年亦是如此。HF作為??偷馁Y深元老,自然也準備了一些小游戲。其中,有個游戲是這樣的:首先,讓小朋友們圍成一個大圈。然后,他隨機指定一個數m,讓編號為0的小朋友開始報數。每次喊到m-1的那個小朋友要出列唱首歌,然后可以在禮品箱中任意的挑選禮物,并且不再回到圈中,從他的下一個小朋友開始,繼續0...m-1報數....這樣下去....直到剩下最后一個小朋友,可以不用表演,并且拿到??兔F的“名偵探柯南”典藏版(名額有限哦!!^_^)。請你試著想下,哪個小朋友會得到這份禮品呢?(注:小朋友的編號是從0到n-1)
如果沒有小朋友,請返回-1
示例1
輸入:5,3
復制返回值:3
方法一:模擬
最開始長度為n,每次刪除一個數,長度變為n-1,如果用數組模擬操作的話,刪除一個數據,涉及大量的數據搬移操作,所以我們可以使用鏈表來模擬操作。
代碼如下:
時間復雜度:O(N^2), 每次刪除一個節點,需要先找到那個節點,然后再刪除,查找的時間復雜度為O(N)
空間復雜度:O(N)
方法二:遞歸
假設f(n, m) 表示最終留下元素的序號。比如上例子中表示為:f(5,3) = 3
首先,長度為 n 的序列會先刪除第 m % n 個元素,然后剩下一個長度為 n - 1 的序列。那么,我們可以遞歸地求解 f(n - 1, m),就可以知道對于剩下的 n - 1 個元素,最終會留下第幾個元素,我們設答案為 x = f(n - 1, m)。
由于我們刪除了第 m % n 個元素,將序列的長度變為 n - 1。當我們知道了 f(n - 1, m) 對應的答案 x 之后,我們也就可以知道,長度為 n 的序列最后一個刪除的元素,應當是從 m % n 開始數的第 x 個元素。因此有 f(n, m) = (m % n + x) % n = (m + x) % n。
當n等于1時,f(1,m) = 0
代碼為:
時間復雜度:O(N)
空間復雜度: O(N)
方法三:迭代法
根據方法二可知,
f[1] = 0
f[2] = (f{1] + m) % 2
f[3] = (f[2] + m) % 3
...
f[n] = (f[n-1] + m) % n
所以代碼如下:
時間復雜度:O(N)
空間復雜度: O(1)
19.撲克牌順子
描述
現在有2副撲克牌,從撲克牌中隨機五張撲克牌,我們需要來判斷一下是不是順子。
有如下規則:
1. A為1,J為11,Q為12,K為13,A不能視為14
2. 大、小王為 0,0可以看作任意牌
3. 如果給出的五張牌能組成順子(即這五張牌是連續的)就輸出true,否則就輸出false。
例如:給出數據[6,0,2,0,4]
中間的兩個0一個看作3,一個看作5 。即:[6,3,2,5,4]
這樣這五張牌在[2,6]區間連續,輸出true
數據保證每組5個數字,每組最多含有4個零,數組的數取值為 [0, 13]
方法一:set+遍歷
我們分兩種情況考慮,
一.?如果vector中不包含0的情況:
那么如何判斷呢?因為需要是順子,所以首先不能有重復值, 如果沒有重復值,那么形如[1 2 3 4 5]
[5 6 7 8 9], 會發現最大值與最小值的差值應該小于5.
二.?如果vector中包含0:
發現除去0后的值,判斷方法和1中是一樣的。
所以根據如上兩個條件,算法過程如下:
代碼如下:
class Solution { public:bool IsContinuous( vector<int> numbers ) {if (numbers.empty()) return false;set<int> st;int max_ = 0, min_ = 14;for (int val : numbers) {if (val > 0) {if (st.count(val) > 0) return false;st.insert(val);max_ = max(max_, val);min_ = min(min_, val);}}return max_ - min_ < 5;} };時間復雜度:O(N)
空間復雜度:O(N)
方法二:排序+遍歷
根據方法一的分析,實現上如果不用set判斷是否有重復值的話,還可以先排序,然后如果有重復值,那么肯定相鄰。
所以代碼如下:
時間復雜度:O(NlogN)
空間復雜度:O(1)
20.?和為S的兩個數字
描述
輸入一個遞增排序的數組和一個數字S,在數組中查找兩個數,使得他們的和正好是S,如果有多對數字的和等于S,輸出兩個數的乘積最小的。
返回值描述:
對應每個測試案例,輸出兩個數,小的先輸出。
方法一:哈希法
要求a + b = sum, 如果已知a, 那么b = sum - a
所以可以先將b添加入哈希中,然后遍歷一遍數組設為a, 在哈希中尋找是否存在sum-a,然后再更新乘積最小值
代碼
class Solution { public:vector<int> FindNumbersWithSum(vector<int> array,int sum) {if (array.empty()) return vector<int>();int tmp = INT_MAX;pair<int, int> ret;unordered_map<int,int> mp;for (int i=0; i<array.size(); ++i) {mp[array[i]] = i;}for (int i=0; i<array.size(); ++i) {if (mp.find(sum-array[i]) != mp.end()) {int j = mp[sum-array[i]];if ( j > i && array[i]*array[j] < tmp) {tmp = array[i] * array[j];ret = {i, j};}}}if (ret.first == ret.second) return vector<int>();return vector<int>({array[ret.first], array[ret.second]});} };時間復雜度:O(n)
空間復雜度:O(n)
方法二:雙指針
假設:若b>a,且存在,
a + b = s;
(a - m ) + (b + m) = s
則:(a - m )(b + m)=ab - (b-a)m - m*m < ab;說明外層的乘積更小
也就是說依然是左右夾逼法!!!只需要2個指針
1.left開頭,right指向結尾
2.如果和小于sum,說明太小了,left右移尋找更大的數
3.如果和大于sum,說明太大了,right左移尋找更小的數
4.和相等,把left和right的數返回
//開始還在糾結乘積最小,后來轉念一想,a+b=sum,a和b越遠乘積越小,而一頭一尾兩個指針往內靠近的方法找到的就是乘積最小的情況。
//如果是乘積最大的情況就是一直找到兩個指針重合,每次找到一個就將之前返回的結果向量清空然后更新為新找到的。
class Solution { public://開始還在糾結乘積最小,后來轉念一想,a+b=sum,a和b越遠乘積越小,而一頭一尾兩個指針往內靠近的方法找到的就是乘積最小的情況。//如果是乘積最大的情況就是一直找到兩個指針重合,每次找到一個就將之前返回的結果向量清空然后更新為新找到的。vector<int> FindNumbersWithSum(vector<int> array,int sum) {vector<int> result;int length = array.size();int start = 0;int end = length - 1;while (start < end) {if (array[start] + array[end] == sum) {result.push_back(array[start]);result.push_back(array[end]);break;} else if (array[start] + array[end] < sum) {start++;} else {end--;}}return result;} };代碼
時間復雜度:O(n)
空間復雜度:O(1)
21.?和為S的連續正數序列
描述
小明很喜歡數學,有一天他在做數學作業時,要求計算出9~16的和,他馬上就寫出了正確答案是100。但是他并不滿足于此,他在想究竟有多少種連續的正數序列的和為100(至少包括兩個數)。沒多久,他就得到另一組連續正數和為100的序列:18,19,20,21,22。現在把問題交給你,你能不能也很快的找出所有和為S的連續正數序列? Good Luck!
返回值描述:
輸出所有和為S的連續正數序列。序列內按照從小至大的順序,序列間按照開始數字從小到大的順序
輸入:9
返回值:[[2,3,4],[4,5]
方法三:滑動窗口
知識補充:
什么是滑動窗口?
顧名思義,首先是一個窗口,既然是一個窗口,就需要用窗口的左邊界i和右邊界j來唯一表示一個窗口,其次,滑動代表,窗口始終從左往右移動,這也表明左邊界i和右邊界j始終會往后移動,而不會往左移動。
這里我用左閉右開區間來表示一個窗口。比如
?
滑動窗口的操作
- 擴大窗口,j += 1
- 縮小窗口,i += 1
算法步驟:
這里需要注意2個問題:
代碼如下:
class Solution { public:vector<vector<int> > FindContinuousSequence(int sum) {vector<vector<int> > allRes;int phigh = 2, plow = 1;while (phigh > plow) {//由于是連續的,差為1的一個序列,那么求和公式是(a0+an)*n/2int cur = (phigh+plow)*(phigh-plow+1)/2;//如果當前窗口內的值之和小于sum,那么右邊窗口右移一下if (cur < sum) {phigh++;} else if (cur > sum) {//如果當前窗口內的值之和大于sum,那么左邊窗口右移一下plow++;} else {//相等,那么就將窗口范圍的所有數添加進結果集,左邊窗口右移一下vector<int> res;for(int i = plow; i<=phigh; i++)res.push_back(i);allRes.push_back(res);plow++;}}} };時間復雜度:O(N)
空間復雜度:O(1)
總結
以上是生活随笔為你收集整理的算法总结之编码(C++)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据库基础-面试
- 下一篇: s3c2440移植MQTT