劍指offer第二章——c++實現 持續更新中
2.1面試官談基礎知識
1、c++的基礎知識(面向物件的特性、建構函式、解構函式、動態繫結、記憶體管理)
2、設計模式
3、uml圖
4、併發控制
5、對os的理解程度
時間複雜度排序:O(1) > O(lognN) > O(n) > O(NlogN) > O(N*N)
2.2 程式語言
c++三種考查方式:
1、考概念(重點考察c++關鍵字的理解程度。
例如,c++中,有哪四個與型別轉換相關的關鍵字?這些關鍵字有什麼特點,適合在什麼場合下使用)
解答:http://www.cnblogs.com/mjiang2017/p/9358032.html
面試場景:
2、分析程式碼的執行結果
淺顯分析:
A b = a;
這句會呼叫拷貝建構函式,因為賦值的時候,b還沒被例項化。
拷貝建構函式引數必須是傳引用的原因分析
如果拷貝建構函式是傳值而不是傳引用,當呼叫A b = a; 時,a作為引數傳值給A(A other),因為other沒有被初始化,所以會繼續呼叫拷貝建構函式,接下來是構造other,也就是A(A other),必然又會有other傳給A(A other),那麼又會觸發拷貝建構函式,這樣就無限遞迴下去了。
所以,拷貝建構函式的引數使用引用型別不是為了減少一次記憶體拷貝,而是避免拷貝建構函式無限制的遞迴下去。
下面這幾種情況下會呼叫拷貝建構函式
(1)顯式或隱式地用同類型的一個物件來初始化另外一個物件。如上例中的 A b = a;
(2)作為實參傳遞給一個函式。如上例中的bbb.myTestFunc(aaa);
(3)在函式體內返回一個物件時,也會呼叫返回值型別的拷貝建構函式
深度剖析
把引數傳遞給函式有三種方法,一種是值傳遞,一種是傳地址,還有一種是傳引用。前者與後兩者不同的地方在於:當使用值傳遞的時候,會在函式裡面生成傳遞引數的一個副本,這個副本的內容是按位從原始引數那裡複製過來的,兩者的內容是相同的。
為什麼會這樣?嗯,一般的建構函式都是會完成一些成員屬性初始化的工作,在物件傳遞給某一函式之前,物件的一些屬性可能已經被改變了,如果在產生物件副本的時候再執行物件的建構函式,那麼這個物件的屬性又再恢復到原始狀態,這並不是我們想要的。所以在產生物件副本的時候,建構函式不會被執行,被執行的是一個預設的建構函式。
當函式執行完畢要返回的時候,物件副本會執行解構函式,如果你的解構函式是空的話,就不會發生什麼問題,但一般的解構函式都是要完成一些清理工作,如釋放指標所指向的記憶體空間。這時候問題就可能要出現了。
假如你在建構函式裡面為一個指標變數分配了記憶體,在解構函式裡面釋放分配給這個指標所指向的記憶體空間,那麼在把物件傳遞給函式至函式結束返回這一過程會發生什麼事情呢?
首先有一個物件的副本產生了,這個副本也有一個指標,它和原始物件的指標是指向同塊記憶體空間的。函式返回時,物件的解構函式被執行了,即釋放了物件副本里面指標所指向的記憶體空間,但是這個記憶體空間對原始物件還是有用的啊,就程式本身而言,這是一個嚴重的錯誤。然而錯誤還沒結束,當原始物件也被銷燬的時候,解構函式再次執行,對同一塊系統動態分配的記憶體空間釋放兩次是一個未知的操作,將會產生嚴重的錯誤。
上面說的就是我們會遇到的問題。解決問題的方法是什麼呢?
首先我們想到的是不要以傳值的方式來傳遞引數,我們可以用傳地址或傳引用。沒錯,這樣的確可以避免上面的情況,而且在允許的情況下,傳地址或傳引用是最好的方法,但這並不適合所有的情況,有時我們不希望在函式裡面的一些操作會影響到函式外部的變數。那要怎麼辦呢?可以利用拷貝建構函式來解決這一問題。拷貝建構函式就是在產生物件副本的時候執行的,我們可以定義自己的拷貝建構函式。在拷貝建構函式裡面我們申請一個新的記憶體空間來儲存建構函式裡面的那個指標所指向的內容。這樣在執行物件副本的解構函式時,釋放的就是複製建構函式裡面所申請的那個記憶體空間。(也就是自己寫一個深拷貝的拷貝建構函式,系統預設生成的拷貝建構函式是淺拷貝的,容易發生記憶體洩漏)
在C++中,下面三種物件需要拷貝的情況。因此,拷貝建構函式將會被呼叫。
1). 一個物件以值傳遞的方式傳入函式體
2). 一個物件以值傳遞的方式從函式返回
3). 一個物件需要通過另外一個物件進行初始化
參考連結:
https://blog.csdn.net/xiaoquantouer/article/details/70145348 (比較淺)
https://my.oschina.net/N3verL4nd/blog/867052#comments (更深入)
面試題一 考點:拷貝賦值函式的書寫
拷貝賦值(賦值運算子)考點:
1、是否把返回值的型別宣告為該型別的引用,並在函式結束前返回例項自身的引用(*this)。只有返回一個引用,才可以執行連續賦值。
2、是否把傳入的引數型別宣告為常量引用。如果傳入的引數不是引用而是例項,那麼從形參到實參會呼叫一次拷貝建構函式,把引數宣告為引用可以避免這樣的無謂消耗。
3、是否釋放例項自身已有的記憶體(不釋放會發生記憶體洩漏)
4、判斷傳入的引數和當前例項(*this)是否為同一個例項。如果是,則不進行復制操作,直接返回。
經典解法
#pragma once
#include <cstring>
class CMyString {
public:
CMyString(char * pData = nullptr);
CMyString(const CMyString& STR);
CMyString& operator= (const CMyString& STR);
~CMyString(void);
private:
char * m_pData;
};
inline
CMyString& CMyString::operator=(const CMyString& str) {
if (this == &str)
return *this;
delete[] m_pData;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
缺點:如果記憶體不足導致new char丟擲異常,則m_pData將是一個空指標。
在拷貝建構函式中實現異常安全性的兩種方法:
1、先用new分配記憶體,在delete原來的內容。
2、先建立一個臨時例項,在交換臨時例項和原來的例項。(推薦)
考慮異常安全性的解法
#pragma once
#include <cstring>
class CMyString {
public:
CMyString(char * pData = nullptr);
CMyString(const CMyString& STR);
CMyString& operator= (const CMyString& STR);
~CMyString(void);
private:
char * m_pData;
};
inline
CMyString& CMyString::operator=(const CMyString& str) {
if (this != &str) {
CMyString strTemp(str);
char * pTemp = strTemp.m_pData;
m_pData = pTemp;
}
return *this;
}
個人擴充套件——既然複習了拷貝賦值函式,那就把建構函式、拷貝建構函式還有解構函式一起復習一下吧
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);
String(const String& str);
String& operator=(const String& str);
~String();
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
#include <cstring>
//建構函式
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
//拷貝構造
inline
String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
//拷貝賦值
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
//解構函式
inline
String::~String()
{
delete[] m_data;
}
#endif
面試場景
面試題二 考點:實現singleton模式
(書上用c#實現的題解,一年沒用c#了,我用c++寫一個)
餓漢模式——利用靜態建構函式建立單例模式
#include<iostream>
class Singleton {
Singleton() {
std::cout << "Sigletion()" << std::endl;
}
//靜態成員,指向Singleton物件的指標。
static Singleton * intance;
public:
//提供靜態共有方法,可以使用類名加域名進行訪問,返回物件指標;
static Singleton* GetSigletion()
{
return intance;
}
};
int main()
{
Singleton *ptr = Singleton::GetSigletion();
system("pause");
return 0;
}
懶漢模式——實現按需建立例項
#include<iostream>
using namespace std;
class Singleton {
Singleton() {
cout << "Sigletion2()" << endl;
}
static Singleton* intance2;
public:
static Singleton* GetSigletion2() {
if (intance2 == NULL) {
intance2 = new Singleton();
cout << "it is once" << endl;
}else{
cout << "it is not once" << endl;
}
return intance2;
}
};
Singleton* Singleton::intance2 = NULL; //先初始化為空,等真正用上這個單例的時候再建立這個例。
int main()
{
Singleton *ptr = Singleton::GetSigletion2();
system("pause");
return 0;
}
擴充套件:懶漢模式、餓漢模式與執行緒安全
連結:https://blog.csdn.net/hj605635529/article/details/70172842
2.3 資料結構
2.3.1 陣列
在c/c++中,當我們宣告一個數組時,其陣列的名字也是一個指標,該指標指向陣列的第一個元素。用指標訪問陣列中的元素時,要小心不要超出陣列的邊界。
1、分析程式碼的執行結果
面試題三 陣列中重複的數字
思路一:先排序,然後從前往後遍歷查詢 時間複雜度O(nlogn)
class Solution {
public:
//手寫快排
void QuickSort(int a[], int l,int r){
if (l >= r) return;
int low = l, high=r;
int key = a[low];
while (low < high) {
//從右往左找,如果該值小於key,則左移到low所指的位置
for (;; high--) {
if (high <= low) break;
if (a[high] < key) {
a[low] = a[high];
break;
}
}
//從左往右找,如果該值大於key,則右移到high所指的位置
for (;; low++) {
if (high <= low)break;
if (a[low] > key) {
a[high] = a[low];
break;
}
}
}
//把key放在low和high重合的位置(即key應該在陣列中的最終位置)
if (low == high) a[low] = key;
//分而治之
QuickSort(a, l, low - 1);
QuickSort(a, low+1, r);
}
//用duplication指向被找到的重複數字
bool duplicate(int numbers[], int length, int* duplication) {
QuickSort(numbers, 0, length );
for(int i=0;i<length-1;i++){
if(numbers[i]==numbers[i+1]){
*duplication=numbers[i];
return true;
}
}
return false;
}
};
每次只有看到這張圖片,我才能心安啊,大哭
思路二 時間複雜度O(n)
從頭到尾掃描陣列,比較數字索引位置為 i 的值 m 是否等於 i ,如果不等,則將該值與索引位置為m的陣列的值進行比較,如果不相等,則將索引位置為 i 的陣列的值與索引位置為 m 的陣列的值進行交換,如果相等,則說明 m 是一個重複的數字。
class Solution {
public:
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
bool duplicate(int numbers[], int length, int* duplication) {
if((numbers==nullptr)||length==0) return false;
for(int i=0;i<length;i++){
if(numbers[i]<0||numbers[i]>length-1)
return false;
}
for(int i=0;i<length;i++){
if(numbers[i]!=i){
if(numbers[numbers[i]]==numbers[i]){
* duplication=numbers[i];
return true;
}
//交換
int temp=numbers[i];
numbers[i]=numbers[temp];
numbers[temp]=temp;
}
}
return false;
}
};
題目變形——不修改陣列找出重複數字
/*
題目:長度為n+1的數組裡,所有的數字都在1~n之間,找出重複的數字
方法:把從1—n的數字,從中間值m分為兩部分,前一半為1—m,後一半為m+1—n。
如果1—m區間中元素的個數超過m,則重複數字出現在該區間,
不斷縮小範圍即可查詢到重複數字
*/
#include "stdafx.h"
#include<iostream>
using namespace std;
int search(int start,int end,int *array,int length) {
if (array == nullptr || length <= 0) return -1;
while (end >= start)
{
int middle = (start + end) / 2;
int count = 0;
for (int i = 0; i < length; i++) {
if (array[i] >= start && array[i] <= middle) count++;
}
if (end == start) {
if (count > 1) return start;
else break;
}
if (count > (middle - start) + 1) end = middle;
else start = middle + 1;
}
return -1;
}
int main()
{
int array[8] = { };
int n = 7, m = 1;
for (int i = 0; i < 8; i++) {
//產生特定範圍的隨機數
//m <= rand() % (n - m + 1) + m <= n
array[i] = rand() % (n - m + 1) + m;
cout << array[i] << endl;
}
//陣列的長度要在呼叫函式之前獲取,因為呼叫函式的時候,陣列退化為指標
int answer = search(1, 8, array, 8);
cout <<"answer為"<< answer << endl;
system("pause");
return 0;
}
面試題四 二維陣列中的查詢
觀察:二維陣列四個角,左上角的數字最小,右下角的數字最大,右上角的數字是這一列裡面最小的,這一行裡面最大的,左下角的數字是這一列裡面最大的,這一行裡面最小的。因此,抓住右上角或者左下角數值的特性,就是本題解題的突破口。
#include <iostream>
#include<vector>
using namespace std;
bool Find(int target, vector<vector<int> > array) {
if (array.empty()) return false;
// array是二維陣列,這裡沒做判空操作
int rows = array.size();
int cols = array[0].size();
int i = 0, j = cols - 1;
//防止陣列越界,i的值等於rows時或者j的值小於0時,迴圈結束
//如此不用擔心陣列越界,因為不合法的索引無法進入迴圈
while (i < rows && j >= 0) {
if (target < array[i][j]) {
j--;
}
else if (target > array[i][j]) {
i++;
}
else {
return true;
}
}
return false;
}
int main()
{
vector<vector<int> >nums = { {1,2,8,9},{2,4,9,12},{4,7,10,13},{6,8,11,15}};
int target = 7;
bool answer = Find(target, nums);
return 0;
}
2.3.2 字串
預備知識
C/C++中,每個字串都以“\0”作為結尾。
為了節省記憶體,C/C++常量字串放在一個單獨的區域。當幾個指標賦值給相同的常量字串時,他們實際上會指向相同的記憶體地址。
面試題:
執行結果:
面試題五 空格替換
思路:先遍歷字串,記錄空格個數,從而計算出新字串的長度。然後從後往前複製各個char。 時間複雜度 O(n)
注意:如果從前往後挪動,不僅挪的char的次數多,還容易發生記憶體覆蓋。
class Solution {
public:
void replaceSpace(char *str,int length) {
if((str==nullptr)||(length==0)) return;
//獲取陣列長度的方法,記錄一下
//int len=sizeof(str)/sizeof(char);
int space_num=0;
for(int i=0;i<length;i++){
if(str[i]==' ') space_num++;
}
//新字串的長度為原字串的長度加上兩倍的空格個數
//兩個長度都減一是為了防止陣列越界
int new_length=length+2*space_num-1;
length-=1;
//開始替換
while(length>=0&&new_length>length){
if(str[length]==' '){
str[new_length--]='0';
str[new_length--]='2';
str[new_length--]='%';
}else{
str[new_length--]=str[length];
}
length--;
}
}
};
2.3.3 連結串列
連結串列結構體
struct ListNode {
int val;
ListNode * next = nullptr;
};
面試題六 從尾到頭列印單鏈表
這種題就很簡單了,實現的方式有很多,比如利用遞迴,利用棧,
遞迴實現程式碼:
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
*/
class Solution {
public:
void helper(vector<int>&answer, ListNode* head){
if(head->next==nullptr) return;
if(answer.empty()){
head=head->next;
helper(answer, head);
}
answer.push_back(head->val);
}
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> answer;
if(head==nullptr) return answer;
helper(answer,head);
//把連結串列頭加入vector中
answer.push_back(head->val);
return answer;
}
};
2.3.4 樹
先來複習一下二叉樹,來一個根據層序遍歷的輸出結果構建二叉樹,然後分別按先序遍歷、中序遍歷和後序遍歷列印二叉樹。
#include <iostream>
#include<vector>
#include<queue>
struct TreeNode {
int val;
TreeNode * left;
TreeNode * right;
TreeNode(int x)
:val(x), left(nullptr), right(nullptr){}
};
//構建二叉樹
TreeNode* CreatTree(std::vector<int>&num) {
//判斷邊界條件
int len = num.size();
if (len == 0) return nullptr;
//資料預處理(建立索引器,索引num中的值)
int index = 0;
//建立根節點
TreeNode *root = new TreeNode(num[index]);
TreeNode * temp = nullptr;
//佇列儲存樹的每個節點,分別為每個節點建立子節點
std::queue<TreeNode*>trans;
//根節點入隊
trans.push(root);
//!trans.empty(),說明還有節點要被插入到二叉樹中
while (!trans.empty() && index < len) {
temp = trans.front();
trans.pop();
//當前陣列中的值非空
if (index < len && num[index] != -1) {
//構造節點
TreeNode *LeftNode = new TreeNode(num[index]);
//將構造出來的節點作為當前節點的左子樹
temp->left = LeftNode;
trans.push(LeftNode);
}
index++;
if (index < len && num[index] != -1) {
TreeNode *RightNode = new TreeNode(num[index]);
temp->right = RightNode;
trans.push(RightNode);;
}
index++;
}
return root;
}
//前序遍歷 根左右
void Preorder(TreeNode *root)
{
//根為空,則跳出函式
if (!root)
return;
//輸出根的值
std::cout << root->val << " ";
//遞迴獲取左子樹的值
Preorder(root->left);
//遞迴獲取右子樹的值
Preorder(root->right);
}
//中序遍歷 左根右
void Midorder(TreeNode *root)
{
//根為空,則跳出函式
if (!root)
return;
//遞迴獲取左子樹的值
Midorder(root->left);
std::cout << root->val << " ";
Midorder(root->right);
}
//後序遍歷 左右根
void Postorder(TreeNode *root)
{
if (!root)
return;
Postorder(root->right);
Postorder(root->left);
std::cout << root->val << " ";
}
int main()
{
std::vector<int> num =
{ 3,5,1,6,2,0,8,-1,-1,7,4,-1,-1,-1,-1 };
TreeNode* Tree = CreatTree(num);
//前序遍歷
Preorder(Tree);
//中序遍歷
Midorder(Tree);
//後序遍歷
Postorder(Tree);
return 0;
}
面試題七 重建二叉樹
思路:用遞迴
先序遍歷結果中,第一個節點為根節點,在中序遍歷結果中找到該節點,則該節點左側的各個節點即為左子樹中的各個節點,相應的找到先序遍歷中左子樹的各個節點;
然後找到先序遍歷中左子樹的各個節點的第一個節點,即為左子樹的根節點,在中序遍歷中左子樹節點的範圍內找左子樹的根節點,該節點左側的即為左子樹的左子樹節點。以此類推。
寫遞迴的時候,要注意先找好遞迴的終止條件是什麼,邊界條件是什麼。
class Solution {
public:
TreeNode* plantTree(vector<int> pre,vector<int> in){
//遞迴的退出條件
if(pre.size()==0||in.size()==0||pre.size()!=in.size())
return NULL;
TreeNode* root = new TreeNode(pre[0]);
int index = 0;
vector<int> left_pre,right_pre,left_in,right_in;
//找到中序遍歷中,根節點的位置
for(int i = 0;i<in.size();i++) {
if(root->val==in[i])
index=i;
}
//左子樹的各個節點
for(int i = 0;i<index;i++) {
left_pre.push_back(pre[i+1]);
left_in.push_back(in[i]);
}
//右子樹的各個節點
for(int j = index+1;j<pre.size();j++) {
right_pre.push_back(pre[j]);
right_in.push_back(in[j]);
}
root->left = plantTree(left_pre,left_in);
root->right = plantTree(right_pre,right_in);
return root;
}
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
if(pre.empty()||vin.empty()) return nullptr;
return plantTree(pre, vin);
}
};
面試題八 二叉樹的下一個節點
題目:給定一個二叉樹和其中一個節點,如何找出中序遍歷序列的下一個節點?樹中節點除了分別指向左右子樹的指標,還有一個指向父節點的指標。
如果當前考察的節點,如果該節點有右子樹,則下一個節點是該節點右子樹的最左子節點;如果該節點為其父節點的左子節點,則該節點的父節點為下一個節點;如果該節點的父節點沒有左子節點,則其父節點的父節點
想起在百度的面試題,樹的結構是隻有指向左孩子的指標和兄弟節點的指標,找特定節點(都是換湯不換藥,遞迴的去遍歷而已)
未完待續。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
char轉整數 -‘0’
整數轉char要考慮正整數還是負整數
strcpy、strstr、strlen、sizeof