目錄
面向對象,寫三個class,動物、狗、貓,你自己發揮(訪問控制權限符、virtual、多態、虛析構巴拉巴拉,然後解釋了一下)
爲什麼哈希表、完全平衡二叉樹、B樹、B+樹都可以優化查詢,爲何Mysql獨獨喜歡B+樹?
調用malloc時會立即分配物理內存嗎?頁表中一定會對應物理頁框嗎?swap交換空間
算法
劍指offer 11、旋轉數組的最小數字
思路:從頭到尾遍歷數組一次,我們就能找出最小的元素。這種思路的時間複雜度顯然是O(n)。但是這個思路沒有利用輸入的旋轉數組的特性,肯定達不到面試官的要求。我們注意到旋轉之後的數組實際上可以劃分爲兩個排序的子數組,而且前面的子數組的元素都大於或者等於後面子數組的元素。我們還注意到最小的元素剛好是這兩個子數組的分界線。在排序的數組中我們可以用二分查找法實現O(logn)的查找。
Step1.和二分查找法一樣,我們用兩個指針分別指向數組的第一個元素和最後一個元素。
Step2.接着我們可以找到數組中間的元素:如果該中間元素位於前面的遞增子數組,那麼它應該大於或者等於第一個指針指向的元素。此時數組中最小的元素應該位於該中間元素的後面。我們可以把第一個指針指向該中間元素,這樣可以縮小尋找的範圍。移動之後的第一個指針仍然位於前面的遞增子數組之中。如果中間元素位於後面的遞增子數組,那麼它應該小於或者等於第二個指針指向的元素。此時該數組中最小的元素應該位於該中間元素的前面。
Step3.接下來我們再用更新之後的兩個指針,重複做新一輪的查找。
public static int GetMin(int[] numbers)
{
if (numbers == null || numbers.Length <= 0)
{
return int.MinValue;
}
int index1 = 0;
int index2 = numbers.Length - 1;
// 把indexMid初始化爲index1的原因:
// 一旦發現數組中第一個數字小於最後一個數字,表明該數組是排序的
// 就可以直接返回第一個數字了
int indexMid = index1;
while (numbers[index1] >= numbers[index2])
{
// 如果index1和index2指向相鄰的兩個數,
// 則index1指向第一個遞增子數組的最後一個數字,
// index2指向第二個子數組的第一個數字,也就是數組中的最小數字
if (index2 - index1 == 1)
{
indexMid = index2;
break;
}
indexMid = (index1 + index2) / 2;
// 特殊情況:如果下標爲index1、index2和indexMid指向的三個數字相等,則只能順序查找
if (numbers[index1] == numbers[indexMid] && numbers[indexMid] == numbers[index2])
{
return GetMinInOrder(numbers, index1, index2);
}
// 縮小查找範圍
if (numbers[indexMid] >= numbers[index1])
{
index1 = indexMid;
}
else if (numbers[indexMid] <= numbers[index2])
{
index2 = indexMid;
}
}
return numbers[indexMid];
}
public static int GetMinInOrder(int[] numbers, int index1, int index2)
{
int result = numbers[index1];
for (int i = index1 + 1; i <= index2; ++i)
{
if (result > numbers[i])
{
result = numbers[i];
}
}
return result;
}
打印字符串中的所有迴文串(要時間複雜度o(n)-希爾排序)
描述
給定一個字符串,輸出所有長度至少爲2的迴文子串。
迴文子串即從左往右輸出和從右往左輸出結果是一樣的字符串,
比如:abba,cccdeedccc都是迴文字符串。
分析:
該題目輸出格式要求比較特別:子串長度小的優先輸出,若長度相等,則出現位置靠左的優先輸出。
所以,這裏分如下幾個步驟來完成任務:
1、枚舉子串的所有可能的長度 for(len=2;len<=n;len++)
2、當長度確定爲len時,枚舉所有長度爲len的子串的開始點。
maxBegin=n-len;
for(begin=0;begin<=maxBegin;begin++)
3、當開始點和長度明確時,可以遍歷該子串並判斷其是否迴文串。
j=begin+len-1;
for(i=begin;i<j;i++,j--)
#include<stdio.h>
#include<string.h>
int main(int argc, char *argv[])
{
char a[505];
int n,len,begin,maxBegin,i,j;
freopen("29.in","r",stdin);
scanf("%s",a);
n=strlen(a);
for(len=2;len<=n;len++)//枚舉子串的所有可能的長度
{
maxBegin=n-len;
for(begin=0;begin<=maxBegin;begin++)//枚舉子串的開始點
{
j=begin+len-1;
for(i=begin;i<j;i++,j--) //遍歷當前子串(a[i]~a[begin+len-1]),判斷是否迴文串
{
if(a[i]!=a[j]) break;
}
if(i>=j)//是迴文串
{
j=begin+len-1;
for(i=begin;i<=j;i++) printf("%c",a[i]);
printf("\n");
}
}
}
return 0;
}
迷宮尋路(dfs和bfs的區別,優缺點)
參考:https://blog.csdn.net/Dog_dream/article/details/80270398
BFS
寬度優先搜索算法(又稱廣度優先搜索)是最簡便的圖的搜索算法之一。其別名又叫BFS,屬於一種盲目搜尋法,目的是系統地展開並檢查圖中的所有節點,以找尋結果。換句話說,它並不考慮結果的可能位置,徹底地搜索整張圖,直到找到結果爲止。
如上所示我要做的就是搜索整張,定義一個二維數組visit, visit[x][y]判斷座標x,y是否被訪問,初始化visit爲0都沒有被訪問;定義一個結構體point裏面的參數有x,y,dis;其中x,y表示座標,dis表示出發點到該節點的步數;
bfs函數操作:
1,將節點添加到隊列裏面;
2,從隊頭取出節點將其訪問狀態設爲1,判斷其上下左右四個節點將符合要求的節點添加到隊列中;
3,重複1,2操作直到從隊列中取出的節點終點返回其dis;
#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<algorithm>
#include<stack>
#include<queue>
#include<iterator>
using namespace std;
#define MAX 10
int x, y, a, b;
struct point
{
int x, y, dis;//x座標y座標步數
};
int fx[4] = { -1,1,0,0 }, fy[4] = { 0,0,-1,1 };
int bfs(int x, int y,int maze[][9])
{
queue<point> myque;
point tp;
tp.x = x;tp.y = y;tp.dis = 0;//初始化開始節點dis設爲0
myque.push(tp);
while (!myque.empty())
{
tp = myque.front();myque.pop();//從隊頭取節點
if (tp.x == a && tp.y == b) { return tp.dis; }//判斷是否到達目的地
for (int i = 0;i < 4;i++)
{
if (tp.x + fx[i] < 9 && tp.x + fx[i] >= 0 && tp.y + fy[i] < 9 &&
tp.y + fy[i] >= 0 && maze[tp.x + fx[i]][tp.y + fy[i]] == 0)
{
point tmp;
tmp.x = tp.x + fx[i];
tmp.y = tp.y + fy[i];
tmp.dis = tp.dis + 1;
maze[tmp.x][tmp.y] = 1;//添加進隊列就將該位置設爲1
myque.push(tmp);
}
}
}
}
int main()
{
int t;
cin >> t;
while (t--)
{
int maze[9][9] =
{ { 1,1,1,1,1,1,1,1,1 },
{ 1,0,0,1,0,0,1,0,1 },
{ 1,0,0,1,1,0,0,0,1 },
{ 1,0,1,0,1,1,0,1,1 },
{ 1,0,0,0,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,0,0,0,1 },
{ 1,1,1,1,1,1,1,1,1 },
};
cin >> x >> y >> a >> b;
cout << bfs(x, y, maze) << endl;
}
return 0;
}
DFS
深度優先搜索屬於圖算法的一種,英文縮寫爲DFS即Depth First Search.其過程簡要來說是對每一個可能的分支路徑深入到不能再深入爲止,而且每個節點只能訪問一次.
顧名思義DFS就是從一個節點出發直到不能訪問然後迴轉到上一層 也就是所說的 回溯+遞歸 實現方法就是從開始節點出發遞歸其四周只有滿足要求就調用函數進行遞歸最後返回;所以我設置了兩個數組dis,visit;dis存放步數,visit判斷是否被訪問;
#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<string>
#include<string.h>
#include<algorithm>
#include<stack>
#include<queue>
#include<iterator>
using namespace std;
#define MAX 10
int maze[9][9] =
{ { 1,1,1,1,1,1,1,1,1 },
{ 1,0,0,1,0,0,1,0,1 },
{ 1,0,0,1,1,0,0,0,1 },
{ 1,0,1,0,1,1,0,1,1 },
{ 1,0,0,0,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,1,0,0,1 },
{ 1,1,0,1,0,0,0,0,1 },
{ 1,1,1,1,1,1,1,1,1 },
};
int x, y, a, b, dis[MAX][MAX],visit[MAX][MAX];
int dfs(int x, int y);
int next(int a,int b,int x, int y)
{
if (dis[x][y] > dis[a][b] + 1) //如果小於就要從這個點從新遍歷
{
dis[x][y] = dis[a][b] + 1;//更新其值
visit[x][y] = 0;//將狀態設爲可訪問 這步比較重要
}
if (!visit[x][y]) { dfs(x, y); }
return 0;
}
// 深度優先
int dfs(int x, int y)
{
visit[x][y] = 1;
if (x + 1 < 9 && maze[x + 1][y] == 0) //上
{
next(x, y, x + 1, y);
}
if (x - 1 >=0 && maze[x - 1][y] == 0) //下
{
next(x, y, x - 1, y);
}
if (y + 1 < 9 && maze[x][y + 1] == 0) //左
{
next(x, y, x, y+1);
}
if (y - 1 >=0 && maze[x][y - 1] == 0) //右
{
next(x, y, x, y-1);
}
return 0;
}
int main()
{
int t;
cin >> t;
while (t--)
{
memset(dis, 1, sizeof(dis));//初始化將dis的值設的比較大
memset(visit, 0, sizeof(visit));//初始化visit將所有值設爲0
cin >> x >> y >> a >> b;
dis[x][y] = 0;
dfs(x, y);
cout << dis[a][b] << endl;
}
return 0;
}
dfs和bfs的區別,優缺點
一般來說是BFS比較快的吧。因爲沒有遞歸,runtime_error一般就是內存溢出,就是越界了! BFS一般用來搜索最短路徑最好,DFS用來搜索能不能到達目的地之類的。
BFS與DFS的討論:BFS:這是一種基於隊列這種數據結構的搜索方式,它的特點是由每一個狀態可以擴展出許多狀態,然後再以此擴展,直到找到目標狀態或者隊列中頭尾指針相遇,即隊列中所有狀態都已處理完畢。
DFS:基於遞歸的搜索方式,它的特點是由一個狀態拓展一個狀態,然後不停拓展,直到找到目標或者無法繼續拓展結束一個狀態的遞歸。
優缺點:
BFS:對於解決最短或最少問題特別有效,而且尋找深度小,但缺點是內存耗費量大(需要開大量的數組單元用來存儲狀態)。
DFS:對於解決遍歷和求所有問題有效,對於問題搜索深度小的時候處理速度迅速,然而在深度很大的情況下效率不高
總結:不管是BFS還是DFS,它們雖然好用,但由於時間和空間的侷限性,以至於它們只能解決數據量小的問題。
字符串複製
#include<iostream>
#include<stdlib.h>
using namespace std;
char *str_cpy(char *dest,char s[])
{
char *p=s;
char *q=dest;
int m=strlen(s);
while(m--!=0)
{
*q++=*p++;
}
*q='\0';
return dest;
}
int main()
{
char a[100];
cout<<"please input a:";
gets_s(a);
cout<<"a:"<<a<<endl;
char *p=(char*)malloc(strlen(a)+1);
str_cpy(p,a);
cout<<"p:"<<p<<endl;
system("pause");
return 0;
}
多線程單例模式
參考:https://zhuanlan.zhihu.com/p/37469260
單例模式(Singleton Pattern,也稱爲單件模式),使用最廣泛的設計模式之一。其意圖是保證一個類僅有一個實例,並提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。
定義一個單例類:
- 私有化它的構造函數,以防止外界創建單例類的對象;
- 使用類的私有靜態指針變量指向類的唯一實例;
- 使用一個公有的靜態方法獲取該實例
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance()
{
if(instance == NULL)
instance = new Singleton();
return instance;
}
};
// init static member
Singleton* Singleton::instance = NULL;
求冪,優化
參考:https://blog.csdn.net/weixin_44048823/article/details/89348856
#include <iostream>
using namespace std;
//利用二進制求x的n次冪,x,n均爲整數;只考慮n的大小
//例如求解2 ^ 10 那麼10轉化爲二進制數字爲1010 對應的位上分別爲 8 4 2 1
//2 ^ 10 可以轉化爲 2 ^ 8 * 2 ^ 2
double Pow(int x, int n)
{
int base = x;
double result = 1;
int y;
if (n == 0)
return 1;
else if(n < 0)
y = -n;
else
y = n;
while (y != 0)
{
//二進制的某一位是1,則乘以現在的base
if (y & 1)
result *= base;
//base平方
base *= base;
//將指數右移1位
y >>= 1;
}
if(n > 0)
return result;
else
return double(1.0/result);
}
int main(int argc, char const *argv[])
{
double r = Pow(5,-2);
cout << r << endl;
return 0;
}
判斷迴文,優化
參考:https://blog.csdn.net/duan19920101/article/details/51481348
給定一個字符串,如何判斷這個字符串是否是迴文串?
思路一:直接在字符串的首尾兩端各放置一個指針*front和*back,然後開始遍歷整個字符串,當*front不再小於*back時完成遍歷。在此過程中,如果出現二者的值不相等,那麼久表示不是迴文串;如果兩個指針指向的字符始終相等就表示該字符串是迴文字符串。
時間複雜度:O(n)
思路二:先使用快慢指針確定出字符串的中間位置,然後分別使用兩個指針從開中間位置開始向相反的方向掃描,知道遍歷完整個字符串。
時間複雜度:O(n)
找中間位置的方法:
1、快慢指針;
2、一種有效的計算方法
//思路一
#include <iostream>
using namespace std;
//*s爲字符串,n爲字符串的長度
bool IsPalindrome(char *str, int n)
{
//指向字符串首尾的指針
char *front = str;
char *back = str + n - 1;
if(str==NULL||n<1)
{
return false;
}
while (front<back)
{
if (*front != *back)
{
return false;
}
front++;
back--;
}
return true;
}
int main( )
{
char str[] = "abba";
int n = strlen(str);
bool sign;
sign = IsPalindrome(str, n);
if (sign == true)
{
cout << "此字符串是迴文字符串"<<endl;
}
else
{
cout << "此字符串不是迴文字符串" << endl;
}
return 0;
}
//思路二
#include <iostream>
using namespace std;
//*s爲字符串,n爲字符串的長度
bool IsPalindrome(char *str, int n)
{
//指向字符串首尾的指針
char *first;
char *second;
if (str == NULL || n<1)
{
return false;
}
//m定位到字符串的中間位置
int m = ((n >> 1) - 1) >= 0 ? (n >> 1) - 1 : 0;
first = str + m;
second= str + n - 1 - m;
while (first>str)
{
if (*first!= *second)
{
return false;
}
first--;
second++;
}
return true;
}
int main( )
{
char str[] = "abcgba";
int n = strlen(str);
bool sign;
sign = IsPalindrome(str, n);
if (sign == true)
{
cout << "此字符串是迴文字符串"<<endl;
}
else
{
cout << "此字符串不是迴文字符串" << endl;
}
return 0;
}
實現快速排序
參考:https://blog.csdn.net/qq_27262727/article/details/104053863
#include<iostream>
using namespace std;
const int N = 1000010;
int n;
int q[N];
//快排模版
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;//判斷邊界,如果輸入只有一個數或者沒有數 就直接return
int x = q[(l + r) / 2], i = l - 1, j = r + 1;//取分界點x(中間點),i和j是指針
while (i < j)//循環迭代,交換,調整區間
{
do i ++; while (q[i] < x);//循環直到q[i]>x
do j --; while (q[j] > x);//循環直到q[i]<=x
//如果循環之後,指針i和j沒有相遇,交換兩個數
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q , j + 1, r);
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(q, 0, n-1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
實現非遞歸後序遍歷二叉樹
參考:https://blog.csdn.net/MBuger/article/details/70186143
思路:利用棧的先入後出的特性,把結點按照前中後序的要求存入棧,再按照需要從棧中彈出。
前序遍歷
大體思路是給出一個棧,從根節點開始向左訪問每個結點並把它們入棧,直到左孩子全被訪問,此時彈出一個結點以和上面同樣的方式訪問其右孩子。直到棧空。
vector<int> NPreOrder(TreeNode* root)
{
vector<int> result;
stack<TreeNode*> s;
if (root == NULL)
return result;
while (root || !s.empty())
{//結束遍歷的條件是root爲空且棧爲空
while(root)
{//找到最左結點,並把路徑上的所有結點一一訪問後入棧
s.push(root);
result.push_back(root->val);
root = root->left;
}
root = s.top();//取棧頂結點
s.pop();//彈出棧頂結點
root = root->right;//左和中都訪問了再往右訪問
}
return result;
}
中序遍歷
中序遍歷和前序遍歷大同小異,只是訪問元素的時間不同,中序遍歷訪問是在元素出棧的時候訪問,而前序遍歷是在元素入棧的時候訪問。
vector<int> NInOrder(TreeNode* root)
{
vector<int> result;
stack<TreeNode*> s;
if (root == NULL)
return result;
while (root || !s.empty())
{
while (root)
{
s.push(root);
root = root->left;
}
root = s.top();
result.push_back(root->val);
s.pop();
root = root->right;
}
return result;
}
後序遍歷
後序遍歷相對複雜一點,因爲後序遍歷訪問一個結點的時候需要滿足兩個條件,一是該結點右孩子爲空,二是該結點的右孩子已經被訪問過,這兩個條件滿足一個則表示該結點可以被訪問。
vector<int> PostOrder(TreeNode* root)
{
vector<int> result;
stack<int> s;
TreeNode* cur = root;
TreeNode* pre = NULL;
if (root == NULL)
return result;
while (cur)
{//走到最左孩子
s.push(cur);
cur = cur->left;
}
while (!s.empty())
{
cur = s.top();
if (cur->right == NULL || cur->right == pre)
{//當一個結點的右孩子爲空或者被訪問過的時候則表示該結點可以被訪問
result.push_back(cur->val);
pre = cur;
s.pop();
}
else
{//否則訪問右孩子
cur = cur->right;
while (cur)
{
s.push(cur);
cur = cur->left;
}
}
}
return result;
}
大數的斐波那契,除留餘數
#include<stdio.h>
int Fib(int n)
{
if(n==1||n==2)
return 1;
else
return (Fib(n-1)+Fib(n-2))%10007;
}
int main()
{
int n=0;
scanf("%d",&n);
printf("%d\n",Fib(n));
return 0;
}
先升序後降序的,找值最快的方法
折半查找變種算法實現
#include <iostream>
int findMax(int* a, int l, int r)
{
int m = (l + r) / 2;
if(a[m] > a[m - 1] && a[m] > a[m + 1])
{
return a[m];
}
else if(a[m] > a[m - 1] && a[m] < a[m + 1])
{
return findMax(a, m + 1, r);
}
else
{
return findMax(a, l, m - 1);
}
}
int main()
{
int a[] = {1,2,3,4,5,8,9,22,6,5,4,1};
int res = findMax(a, 0, 11);
return 0;
}
abcd 10000 一共有多少種可能
參考:https://blog.csdn.net/morewindows/article/details/7370155
給定字符串S[0…N-1],設計算法,枚舉S的全排列。如:123,全排列就是:123,132,213,231,312,321
全排列就是從第一個數字起每個數分別與它後面的數字交換。
//全排列的遞歸實現
#include <stdio.h>
#include <string.h>
void Swap(char *a, char *b)
{
char t = *a;
*a = *b;
*b = t;
}
//k表示當前選取到第幾個數,m表示共有多少數.
void AllRange(char *pszStr, int k, int m)
{
if (k == m)
{
static int s_i = 1;
printf(" 第%3d個排列\t%s\n", s_i++, pszStr);
}
else
{
for (int i = k; i <= m; i++) //第i個數分別與它後面的數字交換就能得到新的排列
{
Swap(pszStr + k, pszStr + i);
AllRange(pszStr, k + 1, m);
Swap(pszStr + k, pszStr + i);
}
}
}
void Foo(char *pszStr)
{
AllRange(pszStr, 0, strlen(pszStr) - 1);
}
int main()
{
printf(" 全排列的遞歸實現\n");
printf(" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
char szTextStr[] = "123";
printf("%s的全排列如下:\n", szTextStr);
Foo(szTextStr);
return 0;
}
//全排列的非遞歸實現
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Swap(char *a, char *b)
{
char t = *a;
*a = *b;
*b = t;
}
//反轉區間
void Reverse(char *a, char *b)
{
while (a < b)
Swap(a++, b--);
}
//下一個排列
bool Next_permutation(char a[])
{
char *pEnd = a + strlen(a);
if (a == pEnd)
return false;
char *p, *q, *pFind;
pEnd--;
p = pEnd;
while (p != a)
{
q = p;
--p;
if (*p < *q) //找降序的相鄰2數,前一個數即替換數
{
//從後向前找比替換點大的第一個數
pFind = pEnd;
while (*pFind <= *p)
--pFind;
//替換
Swap(pFind, p);
//替換點後的數全部反轉
Reverse(q, pEnd);
return true;
}
}
Reverse(p, pEnd);//如果沒有下一個排列,全部反轉後返回true
return false;
}
int QsortCmp(const void *pa, const void *pb)
{
return *(char*)pa - *(char*)pb;
}
int main()
{
printf(" 全排列的非遞歸實現\n");
printf(" --by MoreWindows( http://blog.csdn.net/MoreWindows )--\n\n");
char szTextStr[] = "abc";
printf("%s的全排列如下:\n", szTextStr);
//加上排序
qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp);
int i = 1;
do{
printf("第%3d個排列\t%s\n", i++, szTextStr);
}while (Next_permutation(szTextStr));
return 0;
}
插入排序
#include "stdafx.h"
#include<iostream>
using namespace std;
void InsertSort(int a[], int n)
{
for (int j = 1; j < n; j++)
{
int key = a[j]; //待排序第一個元素
int i = j - 1; //代表已經排過序的元素最後一個索引數
while (i >= 0 && key < a[i])
{
//從後向前逐個比較已經排序過數組,如果比它小,則把後者用前者代替,
//其實說白了就是數組逐個後移動一位,爲找到合適的位置時候便於Key的插入
a[i + 1] = a[i];
i--;
}
a[i + 1] = key;//找到合適的位置了,賦值,在i索引的後面設置key值。
}
}
void main() {
int d[] = { 12, 15, 9, 20, 6, 31, 24 };
cout << "輸入數組 { 12, 15, 9, 20, 6, 31, 24 } " << endl;
InsertSort(d,7);
cout << "排序後結果:";
for (int i = 0; i < 7; i++)
{
cout << d[i]<<" ";
}
}
統計二叉樹第k層的結點數目
參考:https://blog.csdn.net/qq_40550018/article/details/83579158
用子問題遞推的方法,我們可以用第k層左子樹的節點數+第K層右子樹的節點數。
終止條件:空樹返回0,如果查的是第一層的節點數,返回1.
int GetKLevelSize(BNode *root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return GetKLevelSize(root->left, k - 1)+ GetKLevelSize(root->right, k - 1);
}
參考鏈接得細看,包括了二叉樹結點個數、葉子節點個數、第K層節點數、高度求法。
如何計算整個鏈表中節點數量
#ifndef C466_H
#define C466_H
#include<iostream>
using namespace std;
/*
* Count how many nodes in a linked list.
*
* Example
* Given 1->3->5, return 3.
*/
class ListNode{
public:
int val;
ListNode *next;
ListNode(int val){
this->val = val;
this->next = NULL;
}
};
class Solution {
public:
/*
* @param head: the first node of linked list.
* @return: An integer
*/
int countNodes(ListNode * head) {
// write your code here
if (head == NULL)
return 0;
ListNode *p = head;
int len = 0;
while (p != NULL)
{
len++;
p = p->next;
}
return len;
}
};
#endif
實現String
參考:https://coolshell.cn/articles/10478.html
C++ 的一個常見面試題是讓你實現一個 String 類,限於時間,不可能要求具備 std::string 的功能,但至少要求能正確管理資源。具體來說:
- 能像 int 類型那樣定義變量,並且支持賦值、複製。
- 能用作函數的參數類型及返回類型。
- 能用作標準庫容器的元素類型,即 vector/list/deque 的 value_type。(用作 std::map 的 key_type 是更進一步的要求,本文從略)。
換言之,你的 String 能讓以下代碼編譯運行通過,並且沒有內存方面的錯誤。
#include <utility>
#include <string.h>
class String
{
public:
String()
: data_(new char[1])
{
*data_ = '\0';
}
String(const char* str)
: data_(new char[strlen(str) + 1])
{
strcpy(data_, str);
}
String(const String& rhs)
: data_(new char[rhs.size() + 1])
{
strcpy(data_, rhs.c_str());
}
/* Delegate constructor in C++11
String(const String& rhs)
: String(rhs.data_)
{
}
*/
~String()
{
delete[] data_;
}
/* Traditional:
String& operator=(const String& rhs)
{
String tmp(rhs);
swap(tmp);
return *this;
}
*/
String& operator=(String rhs) // yes, pass-by-value
{
swap(rhs);
return *this;
}
// C++ 11
String(String&& rhs)
: data_(rhs.data_)
{
rhs.data_ = nullptr;
}
String& operator=(String&& rhs)
{
swap(rhs);
return *this;
}
// Accessors
size_t size() const
{
return strlen(data_);
}
const char* c_str() const
{
return data_;
}
void swap(String& rhs)
{
std::swap(data_, rhs.data_);
}
private:
char* data_;
};
海量數據中找出出現次數最多的前10個URL
#include<iostream>
#include<string>
#include<map>
#include<vector>
using namespace std;
int main(void)
{
//海量數據
string a[5]={"ab","b","ccc","ab","ccc"};
int n=sizeof(a)/sizeof(a[0]);
cout<<n<<endl;
vector<string> vs(a, a+n);
//哈希統計頻率
map<string,int> mp;
for(int i=0;i<n;i++)
{
if(mp.find(a[i])!=mp.end())
{
mp[vs[i]]++;
}
else
{
mp[vs[i]]=1;
}
}
//對字符串按出現頻率排序
multimap<int,string> multimp;
map<string,int>::iterator it;
for(it=mp.begin();it!=mp.end();it++){
multimp.insert(pair<int,string>(it->second,it->first));
//multimp.insert(it->second,it->first);
}
//輸出出現頻率最高的兩個字符串
multimap<int,string>::iterator mit=multimp.end();
for(int i=1;i<=2;i++)
{
mit--;
cout<<mit->second<<endl;
}
}
topk
#define NUM 10
typedef int ELEM;
void heap(ELEM a[],int left,int right)
{
if (left >= right)
return ;
int r = left;//指向當前的根結點
int LChild = 2*r+1;//指向當前跟結點的左孩子
int Child = LChild;
ELEM temp = a[r];//記錄當前根結點元素
//開始逐漸向下調整
while(LChild <= right)
{
if(LChild < right && a[LChild] > a[LChild+1])
Child = LChild + 1;//Child指向子節點中最小的那個
if(temp > a[Child])
{
a[r] = a[Child];
r = Child;//重新調整跟結點指向
LChild = 2*r+1;//重新調整左孩子指向
Child = LChild;//重新調整孩子指向
}
else
break;
}
a[r] = temp;
return;
}
int main(int argc,char **argv)
{
if(argc < 2)
{
printf("參數數量不夠!");
return 0;
}
int i;
int k = atoi(argv[1]);
ELEM a[NUM] = {2,5,3,1,6,13,15,1859,131,13};
ELEM* b = (ELEM*)malloc(k*sizeof(ELEM));
memset(b,0,k*sizeof(ELEM));
//核心部分
for(i = 0;i < NUM;i++)
{
if(a[i] > b[0])
{
b[0] = a[i];
heap(b,0,k-1);
}
}
//如果還要求最大的k個數有序,將數組b(堆)排序即可.
printf("a中最大的k個數:");
for (i = 0;i < k;i++)
printf("%d ",b[i]);
free(b);
b = NULL;
return 0;
}
面向對象,寫三個class,動物、狗、貓,你自己發揮(訪問控制權限符、virtual、多態、虛析構巴拉巴拉,然後解釋了一下)
#include <iostream>
using namespace std;
class Base
{
public:
virtual void Print() = 0;
virtual ~Base(){}
};
class child_1 : public Base
{
public:
void Print()
{
cout << "child_1 Print function" << endl;
}
~child_1()
{
cout << "child_1 destructor function" << endl;
}
};
class child_2: public Base
{
public:
void Print()
{
cout << "child_2 Print function" << endl;
}
~child_2()
{
cout << "child_2 destructor function" << endl;
}
};
int main()
{
Base *p = new child_1; //父類指針指向子類對象
p->Print();
delete p; //記住釋放,否則內存泄露
p = new child_2;
p->Print();
delete p;
p = NULL;
return 0;
}
希爾排序
希爾排序的實質就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell於1959年提出而得名。
該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因爲直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。
下面給出嚴格按照定義來寫的希爾排序:
//粗糙版本
void shellsort1(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2) //步長
for (i = 0; i < gap; i++) //直接插入排序
{
for (j = i + gap; j < n; j += gap)
if (a[j] < a[j - gap])
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
}
/*改進和優化,以第二次排序爲例,原來是每次從1A到1E,從2A到2E,可以改成從1B開始,先和1A比較,然後取2B與2A比較,再取1C與前面自己組內的數據比較…….。這種每次從數組第gap個元素開始,每個元素與自己組內的數據進行直接插入排序顯然也是正確的。*/
void shellsort2(int a[], int n)
{
int j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (j = gap; j < n; j++)//從數組第gap個元素開始
if (a[j] < a[j - gap])//每個元素與自己組內的數據進行直接插入排序
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
//冒泡思想
void shellsort3(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)
Swap(a[j], a[j + gap]);
}
699個結點的完全二叉樹,有葉子節點多少個?
完全二叉樹(Complete Binary Tree)
若設二叉樹的高度爲h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的節點都連續集中在最左邊,這就是完全二叉樹. 完全二叉樹是由滿二叉樹而引出來的.對於深度爲K的,有N個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一一對應時稱之爲完全二叉樹.
做這種題目你要知道二叉樹的兩個特點!第k層的節點個數最多2^(k-1)個,高度爲k層的二叉樹,最多2^k-1個節點!
則在本題目敏感詞699個節點,因爲是完全二叉樹,2^10-1>699>2^9-1,所以高度爲10,可以確定1到9層全滿,節點總算爲511,剩下的188個肯定爲葉子節點!第10層上的188個節點掛在第九層的188/2=94個節點上,則第九層剩下的2^(9-1)-94=162個也爲葉子節點,最後總共188+162=350個葉子節點!
大魚吃小魚
參考:https://blog.csdn.net/LYJ_viviani/article/details/70163133
有N條魚每條魚的位置及大小均不同,他們沿着X軸遊動,有的向左,有的向右。遊動的速度是一樣的,兩條魚相遇大魚會吃掉小魚。從左到右給出每條魚的大小和遊動的方向(0表示向左,1表示向右)。問足夠長的時間之後,能剩下多少條魚?
#include <bits/stdc++.h>
using namespace std;
int main()
{
stack<int> s;
int n,a,b,num;
cin>>n;
num=n;
while(n--)
{
cin>>a>>b;
if(b==1)
s.push(a);
else
{
while(!s.empty())
{
if(a>s.top())
{
s.pop();
num--;
}
else
{
num--;
break;
}
}
}
}
cout<<num<<endl;
return 0;
}
判斷一個鏈表中是否有環
快慢指針中的快慢指的是移動的步長,即每次向前移動速度的快慢。例如可以讓快指針每次沿鏈表向前移動2,慢指針每次向前移動1次。如果鏈表存在環,就好像操場的跑道是一個環形一樣。此時讓快慢指針都從鏈表頭開始遍歷,快指針每次向前移動兩個位置,慢指針每次向前移動一個位置;如果快指針到達NULL,說明鏈表以NULL爲結尾,沒有環。如果快指針追上慢指針,則表示有環。代碼如下:
bool HasCircle(Node *head)
{
if(head == NULL)
return false;
Node *slow = head, *fast = head;
while(fast != NULL && fast->next!=NULL)
{
slow = slow->next; //慢指針每次前進一步
fast = fast->next->next;//快指針每次前進兩步
if(slow == fast) //相遇,存在環
return true;
}
return false;
}
鏈表反轉
參考:https://blog.csdn.net/blioo/article/details/62050967
總結一下單鏈表的反轉:
- 保存當前頭結點的下個節點。
- 將當前頭結點的下一個節點指向“上一個節點”,這一步是實現了反轉。
- 將當前頭結點設置爲“上一個節點”。
- 將保存的下一個節點設置爲頭結點。
linkList reverse(linkList head){
linkList p,q,pr;
p = head->next;
q = NULL;
head->next = NULL;
while(p){
pr = p->next;
p->next = q;
q = p;
p = pr;
}
head->next = q;
return head;
}
判斷括號是否有效
參考:https://www.cnblogs.com/maoqifansBlog/p/12498130.html
#include <iostream>
#include <stack>
#include <string>
#include <map>
using namespace std;
class Solution
{
public:
bool isValid(string s)
{
if (s == "") // 如果時空字符串也合法
return true;
if (s.size() == 1) // 只有一個字符肯定非法
return false;
stack<char> st;
map<char, char> mp = {{')', '('}, {'}', '{'}, {']', '['}}; // 映射括號
for (int i = 0; i < s.size(); i++)
{
if (mp.find(s[i]) != mp.end()) // 查找mp是否映射了該符號
{
char top_ele = (st.size() == 0) ? '#' : st.top(); // 獲取棧頂元素,若爲空則隨便設置一個字符
if (top_ele != '#') // 棧不爲空則彈出元素
st.pop();
if (top_ele != mp.find(s[i])->second) // 如果這個元素被彈出的元素和mp對應映射的值不一樣,則直接返回false
return false;
}
else
{
st.push(s[i]); //壓入棧中
}
}
return st.size() == 0; // 如果棧爲空則表示合法
}
};
C++基礎
externC
參考:https://www.jianshu.com/p/5d2eeeb93590
extern "C"的主要作用就是爲了能夠正確實現C++代碼調用其他C語言代碼。加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由於C++支持函數重載,因此編譯器編譯函數的過程中會將函數的參數類型也加到編譯後的代碼中,而不僅僅是函數名;而C語言並不支持函數重載,因此編譯C語言代碼的函數時不會帶上函數的參數類型,一般之包括函數名。
標準頭文件如下:
#ifndef __INCvxWorksh /*防止該頭文件被重複引用*/
#define __INCvxWorksh
#ifdef __cplusplus //__cplusplus是cpp中自定義的一個宏
extern "C" { //告訴編譯器,這部分代碼按C語言的格式進行編譯,而不是C++的
#endif
/**** some declaration or so *****/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */
extern "C"的含義
extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。
被extern "C"限定的函數或變量是extern類型的;
1、extern關鍵字
extern是C/C++語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。
通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是並不會報錯;它會在鏈接階段中從模塊A編譯生成的目標代碼中找到此函數。
與extern對應的關鍵字是static,被它修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其不可能被extern “C”修飾。
2、被extern "C"修飾的變量和函數是按照C語言方式編譯和鏈接的
首先看看C++中對類似C的函數是怎樣編譯的。
作爲一種面向對象的語言,C++支持函數重載,而過程式語言C則不支持。函數被C++編譯後在符號庫中的名字與C語言的不同。例如,假設某個函數的原型爲:
void foo( int x, int y );
該函數被C編譯器編譯後在符號庫中的名字爲_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱爲“mangled name”)。
** _foo_int_int這樣的名字包含了函數名、函數參數數量及類型信息,C++就是靠這種機制來實現函數重載的。** 例如,在C++中,函數void foo( int x, int y )與void foo( int x, float y )編譯生成的符號是不相同的,後者爲_foo_int_float。
同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也爲類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。
3、舉例說明
(1)未加extern "C"聲明時的連接方式
假設在C++中,模塊A的頭文件如下:
// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
//在模塊B中引用該函數:
// 模塊B實現文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
實際上,在連接階段,鏈接器會從模塊A生成的目標文件moduleA.obj中尋找_foo_int_int這樣的符號!
(2)加extern "C"聲明後的編譯和鏈接方式
加extern "C"聲明後,模塊A的頭文件變爲:
// 模塊A頭文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模塊B的實現文件中仍然調用foo( 2,3 ),其結果是:
<1>A編譯生成foo的目標代碼時,沒有對其名字進行特殊處理,採用了C語言的方式;
<2>鏈接器在爲模塊B的目標代碼尋找foo(2,3)調用時,尋找的是未經修改的符號名_foo。
如果在模塊A中函數聲明瞭foo爲extern "C"類型,而模塊B中包含的是extern int foo(int x, int y),則模塊B找不到模塊A中的函數;反之亦然。extern “C”這個聲明的真實目的是爲了實現C++與C及其它語言的混合編程。
應用場合
- C++代碼調用C語言代碼、在C++的頭文件中使用
在C++中引用C語言中的函數和變量,在包含C語言頭文件(假設爲cExample.h)時,需進行下列處理:
extern "C"
{
#include "cExample.h"
}
而在C語言的頭文件中,對其外部函數只能指定爲extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現編譯語法錯誤。
/* c語言頭文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y); //注:寫成extern "C" int add(int , int ); 也可以
#endif
/* c語言實現文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++實現文件,調用add:cppFile.cpp
extern "C"
{
#include "cExample.h" //注:此處不妥,如果這樣編譯通不過,換成 extern "C" int add(int , int ); 可以通過
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數時,應加extern "C"{}。
- 在C中引用C++語言中的函數和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明瞭extern "C"的該頭文件,應該僅將C文件中將C++中定義的extern "C"函數聲明爲extern類型
//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++實現文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C實現文件 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
虛函數使用範圍及不同情況
1、什麼是虛函數
C++書中介紹爲了指明某個成員函數具有多態性,用關鍵字virtual來標誌其爲虛函數。傳統的多態實際上就是由虛函數(Virtual Function)利用虛表(Virtual Table)實現的也就是說,虛函數應爲多態而生。
2、什麼時候用到虛函數
既然虛函數虛函數應爲多態而生,那麼簡單的說當我們在C++和C#中要想實現多態的方法之一就是使用到虛函數。複雜點說,那就是因爲OOP的核心思想就是用程序語言描述客觀世界的對象,從而抽象出一個高內聚、低偶合,易於維護和擴展的模型。
但是在抽象過程中我們會發現很多事物的特徵不清楚,或者很容易發生變動,怎麼辦呢?比如飛禽都有飛這個動作,但是對於不同的鳥類它的飛的動作方式是 不同的,有的是滑行,有的要顫抖翅膀,雖然都是飛的行爲,但具體實現卻是千差萬別,在我們抽象的模型中不可能把一個個飛的動作都考慮到,那麼怎樣爲以後留 下好的擴展,怎樣來處理各個具體飛禽類千差萬別的飛行動作呢?比如我現在又要實現一個類“鶴”,它也有飛禽的特徵(比如飛這個行爲),如何使我可以只用簡 單地繼承“飛禽”,而不去修改“飛禽”這個抽象模型現有的代碼,從而達到方便地擴展系統呢?
因此面向對象的概念中引入了虛函數來解決這類問題。
使用虛函數就是在父類中把子類中共有的但卻易於變化或者不清楚的特徵抽取出來,作爲子類需要去重新實現的操作(override)。而虛函數也是OOP中實現多態的關鍵之一。
3、虛函數/接口/抽象函數
虛函數:可由子類繼承並重寫的函數,後期綁定
抽像函數:規定其非虛子類必須實現的函數,必須被重寫。
接口:必須重寫
多態種類和區別
C++ 中的多態性具體體現在編譯和運行兩個階段。編譯時多態是靜態多態,在編譯時就可以確定使用的接口。運行時多態是動態多態,具體引用的接口在運行時才能確定。
靜態多態和動態多態的區別其實只是在什麼時候將函數實現和函數調用關聯起來,是在編譯時期還是運行時期,即函數地址是早綁定還是晚綁定的。靜態多態是指在編譯期間就可以確定函數的調用地址,並生產代碼,這就是靜態的,也就是說地址是早綁定。靜態多態往往也被叫做靜態聯編。 動態多態則是指函數調用的地址不能在編譯器期間確定,需要在運行時確定,屬於晚綁定,動態多態往往也被叫做動態聯編。
多態的作用:爲了接口重用。靜態多態,將同一個接口進行不同的實現,根據傳入不同的參數(個數或類型不同)調用不同的實現。動態多態,則不論傳遞過來的哪個類的對象,函數都能夠通過同一個接口調用到各自對象實現的方法。
靜態多態和動態多態的區別
靜態多態往往通過函數重載和模版(泛型編程)來實現。
動態多態最常見的用法就是聲明基類的指針,利用該指針指向任意一個子類對象,調用相應的虛函數,可以根據指向的子類的不同而調用不同的方法。如果沒有使用虛函數,即沒有利用 C++ 多態性,則利用基類指針調用相應函數的時候,將總被限制在基類函數本身,而無法調用到子類中被重寫的函數。因爲沒有多態性,函數調用的地址將是一定的,而固定的地址將始終調用同一個函數,這就無法達到“一個接口,多種實現”的目的了。
C++內存分配方式,靜態變量在哪兒
棧,就是那些由編譯器在需要的時候分配,在不需要的時候自動清除的變量的存儲區。裏面的變量通常是局部變量、函數參數等。在一個進程中,位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數的調用。和堆一樣,用戶棧在程序執行期間可以動態地擴展和收縮。
堆,就是那些由 new 分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個 new 就要對應一個 delete。如果程序員沒有釋放掉,那麼在程序結束後,操作系統會自動回收。堆可以動態地擴展和收縮。
自由存儲區,就是那些由 malloc 等分配的內存塊,他和堆是十分相似的,不過它是用 free 來結束自己的生命的。
全局/靜態存儲區,全局變量和靜態變量被分配到同一塊內存中,在以前的 C 語言中,全局變量又分爲初始化的和未初始化的(初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量與靜態變量在相鄰的另一塊區域,同時未被初始化的對象存儲區可以通過 void* 來訪問和操縱,程序結束後由系統自行釋放),在 C++ 裏面沒有這個區分了,他們共同佔用同一塊內存區。
常量存儲區,這是一塊比較特殊的存儲區,他們裏面存放的是常量,不允許修改(當然,你要通過非正當手段也可以修改,而且方法很多)
靜態變量存在全區/靜態存儲區。
C++中的static關鍵字
參考:https://www.cnblogs.com/beyondanytime/archive/2012/06/08/2542315.html
分爲面向過程和麪向對象兩種類型
面向過程中的static
靜態全局/局部變量/靜態函數:靜態變量都在全局數據區分配內存,包括後面將要提到的靜態局部變量。
靜態全局變量有以下特點:
• 該變量在全局數據區分配內存;
• 未經初始化的靜態全局變量會被程序自動初始化爲0(自動變量的值是隨機的,除非它被顯式初始化);
• 靜態全局變量在聲明它的整個文件都是可見的,而在文件之外是不可見的;
靜態局部變量有以下特點:
• 該變量在全局數據區分配內存;
• 靜態局部變量在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化;
• 靜態局部變量一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化爲0;
• 它始終駐留在全局數據區,直到程序運行結束。但其作用域爲局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;
靜態函數
在函數的返回類型前加上static關鍵字,函數即被定義爲靜態函數。靜態函數與普通函數不同,它只能在聲明它的文件當中可見,不能被其它文件使用。
定義靜態函數的好處:
• 靜態函數不能被其它文件所用;
• 其它文件中可以定義相同名字的函數,不會發生衝突;
面向對象中的static
靜態數據成員
在類內數據成員的聲明前加上關鍵字static,該數據成員就是類內的靜態數據成員。
#include <iostream.h>
class Myclass
{
public:
Myclass(int a,int b,int c);
void GetSum();
private:
int a,b,c;
static int Sum;//聲明靜態數據成員
};
int Myclass::Sum=0;//定義並初始化靜態數據成員
Myclass::Myclass(int a,int b,int c)
{
this->a=a;
this->b=b;
this->c=c;
Sum+=a+b+c;
}
void Myclass::GetSum()
{
cout<<"Sum="<<Sum<<endl;
}
void main()
{
Myclass M(1,2,3);
M.GetSum();
Myclass N(4,5,6);
N.GetSum();
M.GetSum();
}
靜態數據成員有以下特點:
• 對於非靜態數據成員,每個類對象都有自己的拷貝。而靜態數據成員被當作是類的成員。無論這個類的對象被定義了多少個,靜態數據成員在程序中也只有一份拷貝,由該類型的所有對象共享訪問。也就是說,靜態數據成員是該類的所有對象所共有的。對該類的多個對象來說,靜態數據成員只分配一次內存,供所有對象共用。所以,靜態數據成員的值對每個對象都是一樣的,它的值可以更新;
• 靜態數據成員存儲在全局數據區。靜態數據成員定義時要分配空間,所以不能在類聲明中定義。在Example 5中,語句int Myclass::Sum=0;是定義靜態數據成員;
• 靜態數據成員和普通數據成員一樣遵從public,protected,private訪問規則;
• 因爲靜態數據成員在全局數據區分配內存,屬於本類的所有對象共享,所以,它不屬於特定的類對象,在沒有產生類對象時其作用域就可見,即在沒有產生類的實例時,我們就可以操作它;
• 靜態數據成員初始化與一般數據成員初始化不同。靜態數據成員初始化的格式爲:
<數據類型><類名>::<靜態數據成員名>=<值>
• 類的靜態數據成員有兩種訪問形式:
<類對象名>.<靜態數據成員名> 或 <類類型名>::<靜態數據成員名>
如果靜態數據成員的訪問權限允許的話(即public的成員),可在程序中,按上述格式來引用靜態數據成員 ;
• 靜態數據成員主要用在各個對象都有相同的某項屬性的時候。比如對於一個存款類,每個實例的利息都是相同的。所以,應該把利息設爲存款類的靜態數據成員。這有兩個好處,第一,不管定義多少個存款類對象,利息數據成員都共享分配在全局數據區的內存,所以節省存儲空間。第二,一旦利息需要改變時,只要改變一次,則所有存款類對象的利息全改變過來了;
• 同全局變量相比,使用靜態數據成員有兩個優勢:
1. 靜態數據成員沒有進入程序的全局名字空間,因此不存在與程序中其它全局名字衝突的可能性;
靜態成員函數
與靜態數據成員一樣,我們也可以創建一個靜態成員函數,它爲類的全部服務而不是爲某一個類的具體對象服務。靜態成員函數與靜態數據成員一樣,都是類的內部實現,屬於類定義的一部分。普通的成員函數一般都隱含了一個this指針,this指針指向類的對象本身,因爲普通成員函數總是具體的屬於某個類的具體對象的。通常情況下,this是缺省的。如函數fn()實際上是this->fn()。但是與普通函數相比,靜態成員函數由於不是與任何的對象相聯繫,因此它不具有this指針。從這個意義上講,它無法訪問屬於類對象的非靜態數據成員,也無法訪問非靜態成員函數,它只能調用其餘的靜態成員函數。
class Myclass
{
public:
Myclass(int a,int b,int c);
static void GetSum();/聲明靜態成員函數
private:
int a,b,c;
static int Sum;//聲明靜態數據成員
};
int Myclass::Sum=0;//定義並初始化靜態數據成員
Myclass::Myclass(int a,int b,int c)
{
this->a=a;
this->b=b;
this->c=c;
Sum+=a+b+c; //非靜態成員函數可以訪問靜態數據成員
}
void Myclass::GetSum() //靜態成員函數的實現
{
// cout<<a<<endl; //錯誤代碼,a是非靜態數據成員
cout<<"Sum="<<Sum<<endl;
}
void main()
{
Myclass M(1,2,3);
M.GetSum();
Myclass N(4,5,6);
N.GetSum();
Myclass::GetSum();
}
關於靜態成員函數,可以總結爲以下幾點:
• 出現在類體外的函數定義不能指定關鍵字static;
• 靜態成員之間可以相互訪問,包括靜態成員函數訪問靜態數據成員和訪問靜態成員函數;
• 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員;
• 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員;
• 由於沒有this指針的額外開銷,因此靜態成員函數與類的全局函數相比速度上會有少許的增長;
• 調用靜態成員函數,可以用成員訪問操作符(.)和(->)爲一個類的對象或指向類對象的指針調用靜態成員函數,也可以直接使用如下格式:
<類名>::<靜態成員函數名>(<參數表>)
調用類的靜態成員函數。
指針引用
參考:https://www.cnblogs.com/li-peng/p/4116349.html
在C和C++中,指針一般指的是某塊內存的地址,通過這個地址,我們可以尋址到這塊內存;而引用是一個變量的別名,例如我們給小明起了個外號:明明,那我們說明明的時候,就是說小明。
智能指針
參考:https://www.cnblogs.com/TenosDoIt/p/3456704.html
C++11:https://www.cnblogs.com/wxquare/p/4759020.html
C++程序設計中使用堆內存是非常頻繁的操作,堆內存的申請和釋放都由程序員自己管理。程序員自己管理堆內存可以提高了程序的效率,但是整體來說堆內存的管理是麻煩的,C++11中引入了智能指針的概念,方便管理堆內存。使用普通指針,容易造成堆內存泄露(忘記釋放),二次釋放,程序發生異常時內存泄露等問題等,使用智能指針能更好的管理堆內存。
理解智能指針需要從下面三個層次:
- 從較淺的層面看,智能指針是利用了一種叫做RAII(資源獲取即初始化)的技術對普通的指針進行封裝,這使得智能指針實質是一個對象,行爲表現的卻像一個指針。
- 智能指針的作用是防止忘記調用delete釋放內存和程序異常的進入catch塊忘記釋放內存。另外指針的釋放時機也是非常有考究的,多次釋放同一個指針會造成程序崩潰,這些都可以通過智能指針來解決。
- 智能指針還有一個作用是把值語義轉換成引用語義。
智能指針在C++11版本之後提供,包含在頭文件<memory>中,shared_ptr、unique_ptr、weak_ptr
auto_ptr
class Test
{
public:
Test(string s)
{
str = s;
cout<<"Test creat\n";
}
~Test()
{
cout<<"Test delete:"<<str<<endl;
}
string& getStr()
{
return str;
}
void setStr(string s)
{
str = s;
}
void print()
{
cout<<str<<endl;
}
private:
string str;
};
int main()
{
auto_ptr<Test> ptest(new Test("123"));
ptest->setStr("hello ");
ptest->print();
ptest.get()->print();
ptest->getStr() += "world !";
(*ptest).print();
ptest.reset(new Test("123"));
ptest->print();
return 0;
}
如上面的代碼:智能指針可以像類的原始指針一樣訪問類的public成員,成員函數get()返回一個原始的指針,成員函數reset()重新綁定指向的對象,而原來的對象則會被釋放。注意我們訪問auto_ptr的成員函數時用的是“.”,訪問指向對象的成員時用的是“->”。我們也可用聲明一個空智能指針auto_ptr<Test>ptest();
當我們對智能指針進行賦值時,如ptest2 = ptest,ptest2會接管ptest原來的內存管理權,ptest會變爲空指針,如果ptest2原來不爲空,則它會釋放原來的資源,基於這個原因,應該避免把auto_ptr放到容器中,因爲算法對容器操作時,很難避免STL內部對容器實現了賦值傳遞操作,這樣會使容器中很多元素被置爲NULL。判斷一個智能指針是否爲空不能使用if(ptest == NULL),應該使用if(ptest.get() == NULL)。
unique_ptr
unique_ptr 是一個獨享所有權的智能指針,它提供了嚴格意義上的所有權,包括:
1、擁有它指向的對象
2、無法進行復制構造,無法進行復制賦值操作。即無法使兩個unique_ptr指向同一個對象。但是可以進行移動構造和移動賦值操作
3、保存指向某個對象的指針,當它本身被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象
unique_ptr 可以實現如下功能:
1、爲動態申請的內存提供異常安全
2、講動態申請的內存所有權傳遞給某函數
3、從某個函數返回動態申請內存的所有權
4、在容器中保存指針
5、auto_ptr 應該具有的功能
unique_ptr<Test> fun()
{
return unique_ptr<Test>(new Test("789"));
}
int main()
{
unique_ptr<Test> ptest(new Test("123"));
unique_ptr<Test> ptest2(new Test("456"));
ptest->print();
ptest2 = std::move(ptest);//不能直接ptest2 = ptest
if(ptest == NULL)cout<<"ptest = NULL\n";
Test* p = ptest2.release();
p->print();
ptest.reset(p);
ptest->print();
ptest2 = fun(); //這裏可以用=,因爲使用了移動構造函數
ptest2->print();
return 0;
}
share_ptr
從名字share就可以看出了資源可以被多個指針共享,它使用計數機制來表明資源被幾個指針共享。可以通過成員函數use_count()來查看資源的所有者個數。出了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr來構造。當我們調用release()時,當前指針會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。
int main()
{
shared_ptr<Test> ptest(new Test("123"));
shared_ptr<Test> ptest2(new Test("456"));
cout<<ptest2->getStr()<<endl;
cout<<ptest2.use_count()<<endl;
ptest = ptest2;//"456"引用次數加1,“123”銷燬
ptest->print();
cout<<ptest2.use_count()<<endl;//2
cout<<ptest.use_count()<<endl;//2
ptest.reset();
ptest2.reset();//此時“456”銷燬
cout<<"done !\n";
return 0;
}
weak_ptr
weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能下降爲0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
迭代器使用(map set vector list)
答:vector,支持下標,內存連續,resize,capacity,還分析push_back
vector特點是:其容量在需要時可以自動分配,本質上是數組形式的存儲方式。即在索引可以在常數時間內完成。缺點是在插入或者刪除一項時,需要線性時間。但是在尾部插入或者刪除,是常數時間的。
list 是雙向鏈表:如果知道位置,在其中進行插入和刪除操作時,是常數時間的。索引則需要線性時間(和單鏈表一樣)。
vector 和 list 都支持在常量的時間內在容器的末尾添加或者刪除項,vector和list都支持在常量的時間內訪問表的前端的項.
vector會一次分配多個元素內存,那麼下次增加時,只是改寫內存而已,就不會再分配內存了,但是list每次只分配一個元素的內存,每次增加一個元素時,都會執行一次內存分配行爲。如果是大量數據追加,建議使用list,因爲vector 在有大量元素,並且內存已滿,再pushback元素時,需要分配大塊內存並把已經有的數據move到新分配的內存中去(vector不能加入不可複製元素)然後再釋放原來的內存塊,這些操作很耗時。而list的性能而會始終於一的,不會出現vector的性能變化情況,所以對於容器構件,需要用什麼類型最好,取決於業務邏輯。
Map也是一種關聯容器,它是 鍵—值對的集合,即它的存儲都是以一對鍵和值進行存儲的,Map通常也可以理解爲關聯數組(associative array),就是每一個值都有一個鍵與之一一對應,因此,map也是不允許重複元素出現的。
底層數據結構是紅黑樹。具體理解參考:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md
插入數據幾種方式
方法一:pair
例:
map<int, string> mp;
mp.insert(pair<int,string>(1,"aaaaa"));
方法二:make_pair
例:
map<int, string> mp;
mp.insert(make_pair<int,string>(2,"bbbbb"));
方法三:value_type
例:
map<int, string> mp;
mp.insert(map<int, string>::value_type(3,"ccccc"));
方法四:[]
例:
map<int, string> mp;
mp[4] = "ddddd";
四種方法異同:前三種方法當出現重複鍵時,編譯器會報錯,而第四種方法,當鍵重複時,會覆蓋掉之前的鍵值對。
vector與list元素個數相同,遍歷一遍,哪個快
vector更快,list需要指針,vector是連續內存,因爲有L1L2L3緩存,可以更快,緩存層級,金字塔,經常使用的話放在更上一層緩存,讀取就更快 .
紅黑樹和平衡二叉樹區別
AVL是嚴格平衡,旋轉可能更多,紅黑樹的優勢在於穩定性,插入和刪除都是O(logn)
哈希表
參考:https://blog.csdn.net/weixin_38169413/article/details/81612307
概念:根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
構造方法:直接定址法、除留取餘法、平方取中法、摺疊法等。
解決衝突方法
- 換個位置:
開放地址法
- 同一位置的衝突對象組織在一起:
鏈地址法
STL的allocator
參考:https://zhuanlan.zhihu.com/p/34725232
allocator是STL的重要組成,但是一般用戶不怎麼熟悉他,因爲allocator隱藏在所有容器(包括vector)身後,默默完成內存配置與釋放,對象構造和析構的工作。
new和malloc的內存對齊
大多數情況下,編譯器和C庫透明地幫你處理對齊問題。POSIX 標明瞭通過malloc( ), calloc( ), 和 realloc( ) 返回的地址對於任何的C類型來說都是對齊的。
對齊參數(MALLOC_ALIGNMENT) 大小的設定並需滿足兩個特性
1.必須是2的冪
2.必須是(void *)的整數倍
至於爲什麼會要求是(void *)的整數倍,這個目前我還不太清楚,等你來發現...
根據這個原理,在32位和64位的對齊單位分別爲8字節和16字節
但是這並解釋不了上面的測試結果,這是因爲系統malloc分配的最小單位(MINSIZE)並不是對齊單位。
在32位系統中MINSIZE爲16字節,在64位系統中MINSIZE一般爲32字節。從request2size還可以知道,如果是64位系統,申請內存爲1~24字節時,系統內存消耗32字節,當申請內存爲25字節時,系統內存消耗48字節。 如果是32位系統,申請內存爲1~12字節時,系統內存消耗16字節,當申請內存爲13字節時,系統內存消耗24字節。
特化,偏特化
參考:https://www.jianshu.com/p/4be97bf7a3b9
有時爲了需要,針對特定的類型,需要對模板進行特化,也就是所謂的特殊處理。
那模板的偏特化呢?所謂的偏特化是指提供另一份template定義式,而其本身仍爲templatized;也就是說,針對template參數更進一步的條件限制所設計出來的一個特化版本。這種偏特化的應用在STL中是隨處可見的。與模板特化的區別在於,模板特化以後,實際上其本身已經不是templatized,而偏特化,仍然帶有templatized。
虛函數存在哪個內存區
1.虛函數表是全局共享的元素,即全局僅有一個.
2.虛函數表類似一個數組,類對象中存儲vptr指針,指向虛函數表.即虛函數表不是函數,不是程序代碼,不肯能存儲在代碼段.
3.虛函數表存儲虛函數的地址,即虛函數表的元素是指向類成員函數的指針,而類中虛函數的個數在編譯時期可以確定,即虛函數表的大小可以確定,即大小是在編譯時期確定的,不必動態分配內存空間存儲虛函數表,所以不再堆中.
根據以上特徵,虛函數表類似於類中靜態成員變量.靜態成員變量也是全局共享,大小確定.
所以我推測虛函數表和靜態成員變量一樣,存放在全局數據區.
c/c++程序所佔用的內存一共分爲五種:
棧區,堆區,程序代碼區,全局數據區(靜態區),文字常量區.
顯而易見,虛函數表存放在全局數據區.
幾個值得注意的問題
- 虛函數表是class specific的,也就是針對一個類來說的,這裏有點像一個類裏面的staic成員變量,即它是屬於一個類所有對象的,不是屬於某一個對象特有的,是一個類所有對象共有的。
- 虛函數表是編譯器來選擇實現的,編譯器的種類不同,可能實現方式不一樣,就像前面我們說的vptr在一個對象的最前面,但是也有其他實現方式,不過目前gcc 和微軟的編譯器都是將vptr放在對象內存佈局的最前面。
- 雖然我們知道vptr指向虛函數表,那麼虛函數表具體存放在內存哪個位置呢,雖然這裏我們已經可以得到虛函數表的地址。實際上虛函數指針是在構造函數執行時初始化的,而虛函數表是存放在可執行文件中的。下面的一篇博客測試了微軟的編譯器將虛函數表存放在了目標文件或者可執行文件的常量段中,http://blog.csdn.net/vicness/article/details/3962767,不過我在gcc下的彙編文件中沒有找到vtbl的具體存放位置,主要是對可執行文件的裝載和運行原理還沒有深刻的理解,相信不久有了這些知識之後會很輕鬆的找到虛函數表到底存放在目標文件的哪一個段中。
- 經過測試,在gcc編譯器的實現中虛函數表vtable存放在可執行文件的只讀數據段.rodata中。
虛函數表vtable在Linux/Unix中存放在可執行文件的只讀數據段中(rodata),這與微軟的編譯器將虛函數表存放在常量段存在一些差別。
數據庫
數據庫面試題彙總參考:https://blog.csdn.net/qq_27262727/article/details/104875370
mysql底層索引及爲什麼用這個索引
參考:https://juejin.im/post/5c822b0ce51d453a42155c3d
索引概念:索引就像書的目錄Mysql官網是這麼定義的:Indexes are used to find rows with specific column values quickly我是這麼定義的:索引是一種優化查詢的數據結構
爲什麼哈希表、完全平衡二叉樹、B樹、B+樹都可以優化查詢,爲何Mysql獨獨喜歡B+樹?
哈希表
哈希表(Hash table,也叫散列表),是根據鍵值(Key value)而直接進行訪問的數據結構。也就是說,它通過把鍵值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。哈希表的做法其實很簡單,就是把Key通過一個固定的算法函數既所謂的哈希函數轉換成一個整型數字,然後就將該數字對數組長度進行取餘,取餘結果就當作數組的下標,將value存儲在以該數字爲下標的數組空間裏。而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換爲對應的數組下標,並定位到該空間獲取value,如此一來,就可以充分利用到數組的定位性能進行數據定位。
問題:容易出現哈希衝突,哈希表的特點就是可以快速的精確查詢,但是不支持範圍查詢。
完全平衡二叉樹
圖中的每一個節點實際上應該有四部分:
- 左指針,指向左子樹
- 鍵值
- 鍵值所對應的數據的存儲地址
- 右指針,指向右子樹
另外需要提醒的是,二叉樹是有順序的,簡單的說就是“左邊的小於右邊的”假如我們現在來查找‘周瑜’,需要找2次(第一次曹操,第二次周瑜),比哈希表要多一次。而且由於完全平衡二叉樹是有序的,所以也是支持範圍查找的。
B樹
還是上面的表數據用B樹表示如下圖(爲了簡單,數據對應的地址就不畫在圖中了。):
我們可以發現同樣的元素,B樹的表示要比完全平衡二叉樹要“矮”,原因在於B樹中的一個節點可以存儲多個元素。
B+樹
還是上面的表數據用B+樹表示如下圖(爲了簡單,數據對應的地址就不畫在圖中了。):
我們可以發現同樣的元素,B+樹的表示要比B樹要“胖”,原因在於B+樹中的非葉子節點會冗餘一份在葉子節點中,並且葉子節點之間用指針相連。
B+樹到底有什麼優勢呢?
這裏我們用“反證法”,假如我們現在就用完全平衡二叉樹作爲索引的數據結構,我們來看一下有什麼不妥的地方。實際上,索引也是很“大”的,因爲索引也是存儲元素的,我們的一個表的數據行數越多,那麼對應的索引文件其實也是會很大的,實際上也是需要存儲在磁盤中的,而不能全部都放在內存中,所以我們在考慮選用哪種數據結構時,我們可以換一個角度思考,哪個數據結構更適合從磁盤中讀取數據,或者哪個數據結構能夠提高磁盤的IO效率。回頭看一下完全平衡二叉樹,當我們需要查詢“張飛”時,需要以下步驟
- 從磁盤中取出“曹操”到內存,CPU從內存取出數據進行筆記,“張飛”<“曹操”,取左子樹(產生了一次磁盤IO)
- 從磁盤中取出“周瑜”到內存,CPU從內存取出數據進行筆記,“張飛”>“周瑜”,取右子樹(產生了一次磁盤IO)
- 從磁盤中取出“孫權”到內存,CPU從內存取出數據進行筆記,“張飛”>“孫權”,取右子樹(產生了一次磁盤IO)
- 從磁盤中取出“黃忠”到內存,CPU從內存取出數據進行筆記,“張飛”=“張飛”,找到結果(產生了一次磁盤IO)
同理,回頭看一下B樹,我們發現只發送三次磁盤IO就可以找到“張飛”了,這就是B樹的優點:一個節點可以存儲多個元素,相對於完全平衡二叉樹所以整棵樹的高度就降低了,磁盤IO效率提高了。而,B+樹是B樹的升級版,只是把非葉子節點冗餘一下,這麼做的好處是爲了提高範圍查找的效率。
所以,到這裏,我們可以總結出來,Mysql選用B+樹這種數據結構作爲索引,可以提高查詢索引時的磁盤IO效率,並且可以提高範圍查詢的效率,並且B+樹裏的元素也是有序的。
mysql 事務ACID
參考:https://www.cnblogs.com/kismetv/p/10331633.html
mysql事務
概念:事務(Transaction)是訪問和更新數據庫的程序執行單元;事務中可能包含一個或多個sql語句,這些語句要麼都執行,要麼都不執行。作爲一個關係型數據庫,MySQL支持事務。
MySQL服務器邏輯架構從上往下可以分爲三層:
(1)第一層:處理客戶端連接、授權認證等。
(2)第二層:服務器層,負責查詢語句的解析、優化、緩存以及內置函數的實現、存儲過程等。
(3)第三層:存儲引擎,負責MySQL中數據的存儲和提取。MySQL中服務器層不管理事務,事務是由存儲引擎實現的。MySQL支持事務的存儲引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最爲廣泛;其他存儲引擎不支持事務,如MyIsam、Memory等。
ACID特性
ACID是衡量事務的四個特性:
- 原子性(Atomicity,或稱不可分割性):一個事務是一個不可分割的工作單位,其中的操作要麼都做,要麼都不做;如果事務中一個sql語句執行失敗,則已執行的語句也必須回滾,數據庫退回到事務前的狀態。
- 一致性(Consistency):事務一旦提交,它對數據庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。
- 隔離性(Isolation):事務內部的操作與其他事務是隔離的,併發執行的各個事務之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可串行化),但實際應用中出於性能方面的考慮很少會使用可串行化。
- 持久性(Durability):事務執行結束後,數據庫的完整性約束沒有被破壞,事務執行的前後都是合法的數據狀態。數據庫的完整性約束包括但不限於:實體完整性(如行的主鍵存在且唯一)、列完整性(如字段的類型、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉賬前後,兩個賬戶餘額的和應該不變)。
按照嚴格的標準,只有同時滿足ACID特性纔是事務;但是在各大數據庫廠商的實現中,真正滿足ACID的事務少之又少。例如MySQL的NDB Cluster事務不滿足持久性和隔離性;InnoDB默認事務隔離級別是可重複讀,不滿足隔離性;Oracle默認的事務隔離級別爲READ COMMITTED,不滿足隔離性……因此與其說ACID是事務必須滿足的條件,不如說它們是衡量事務的四個維度。
mysql隔離級別及實現
參考:https://www.jianshu.com/p/dab1c0ecbac0
有四種事務隔離級別:
Read uncommitted 讀未提交,顧名思義,就是一個事務可以讀取另一個未提交事務的數據。
Read committed 讀提交,顧名思義,就是一個事務要等另一個事務提交後才能讀取數據。
Repeatable read 重複讀,就是在開始讀取數據(事務開啓)時,不再允許修改操作
Serializable 串行讀,是最高的事務隔離級別,在該級別下,事務串行化順序執行,可以避免髒讀、不可重複讀與幻讀。但是這種事務隔離級別效率低下,比較耗數據庫性能,一般不使用。
查詢是否使用索引(explain)
參考:https://blog.csdn.net/vtopqx/article/details/86504206
mysql> explain SELECT * FROM tb_user
WHERE STATUS=1 limit 0,20;
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
| 1 | SIMPLE | tb_news_online | NULL | ref | idx_tb_news_online_9 | idx_tb_news_online_9 | 5 | const | 99494 | 100 | NULL |
+----+-------------+----------------+------------+------+----------------------+----------------------+---------+-------+-------+----------+-------+
1 row in set
mysql>
EXPLAIN列的解釋:
table:顯示這一行的數據是關於哪張表的
type:這是重要的列,顯示連接使用了何種類型。從最好到最差的連接類型爲const、eq_reg、ref、range、index和ALL
type顯示的是訪問類型,是較爲重要的一個指標,結果值從好到壞依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般來說,得保證查詢至少達到range級別,最好能達到ref。
possible_keys:顯示可能應用在這張表中的索引。如果爲空,沒有可能的索引。可以爲相關的域從WHERE語句中選擇一個合適的語句
key: 實際使用的索引。如果爲NULL,則沒有使用索引。很少的情況下,MYSQL會選擇優化不足的索引。這種情況下,可以在SELECT語句中使用USE INDEX(indexname)來強制使用一個索引或者用IGNORE INDEX(indexname)來強制MYSQL忽略索引
key_len:使用的索引的長度。在不損失精確性的情況下,長度越短越好
ref:顯示索引的哪一列被使用了,如果可能的話,是一個常數
rows:MYSQL認爲必須檢查的用來返回請求數據的行數
Extra:關於MYSQL如何解析查詢的額外信息。將在表4.3中討論,但這裏可以看到的壞的例子是Using temporary和Using filesort,意思MYSQL根本不能使用索引,結果是檢索會很慢
sql優化
參考:
https://database.51cto.com/art/200904/118526.htm
https://www.cnblogs.com/yunfeifei/p/3850440.html
-
查詢語句無論是使用哪種判斷條件 等於、小於、大於,
WHERE
左側的條件查詢字段不要使用函數或者表達式 -
使用
EXPLAIN
命令優化你的 SELECT 查詢,對於複雜、效率低的 sql 語句,我們通常是使用 explain sql 來分析這條 sql 語句,這樣方便我們分析,進行優化。 -
當你的 SELECT 查詢語句只需要使用一條記錄時,要使用
LIMIT 1
-
不要直接使用
SELECT *
,而應該使用具體需要查詢的表字段,因爲使用 EXPLAIN 進行分析時,SELECT * 使用的是全表掃描,也就是type = all
。 -
爲每一張表設置一個 ID 屬性
-
避免在
WHERE
字句中對字段進行NULL
判斷 -
避免在
WHERE
中使用!=
或<>
操作符 -
使用
BETWEEN AND
替代IN
-
爲搜索字段創建索引
-
選擇正確的存儲引擎,InnoDB 、MyISAM 、MEMORY 等
-
使用
LIKE %abc%
不會走索引,而使用LIKE abc%
會走索引 -
對於枚舉類型的字段(即有固定羅列值的字段),建議使用
ENUM
而不是VARCHAR
,如性別、星期、類型、類別等 -
拆分大的 DELETE 或 INSERT 語句
-
選擇合適的字段類型,選擇標準是 儘可能小、儘可能定長、儘可能使用整數。
-
字段設計儘可能使用
NOT NULL
-
進行水平切割或者垂直分割
Mysql MyISAM和InnoDB的區別
參考:https://blog.csdn.net/qq_27262727/article/details/104875370
關於二者的對比與總結:
count運算上的區別:因爲MyISAM緩存有表meta-data(行數等),因此在做COUNT(*)時對於一個結構很好的查詢是不需要消耗多少資源的。而對於InnoDB來說,則沒有這種緩存。
是否支持事務和崩潰後的安全恢復: MyISAM 強調的是性能,每次查詢具有原子性,其執行數度比InnoDB類型更快,但是不提供事務支持。但是InnoDB 提供事務支持事務,外部鍵等高級數據庫功能。 具有事務(commit)、回滾(rollback)和崩潰修復能力(crash recovery capabilities)的事務安全(transaction-safe (ACID compliant))型表。
是否支持外鍵: MyISAM不支持,而InnoDB支持。
MyISAM更適合讀密集的表,而InnoDB更適合寫密集的的表。 在數據庫做主從分離的情況下,經常選擇MyISAM作爲主庫的存儲引擎。 一般來說,如果需要事務支持,並且有較高的併發讀取頻率(MyISAM的表鎖的粒度太大,所以當該表寫併發量較高時,要等待的查詢就會很多了),InnoDB是不錯的選擇。如果你的數據量很大(MyISAM支持壓縮特性可以減少磁盤的空間佔用),而且不需要支持事務時,MyISAM是最好的選擇。
MyISAM 存儲引擎的特點
在 5.1 版本之前,MyISAM 是 MySQL 的默認存儲引擎,MyISAM 併發性比較差,使用的場景比較少,主要特點是
-
不支持
事務
操作,ACID 的特性也就不存在了,這一設計是爲了性能和效率考慮的。 -
不支持
外鍵
操作,如果強行增加外鍵,MySQL 不會報錯,只不過外鍵不起作用。 -
MyISAM 默認的鎖粒度是
表級鎖
,所以併發性能比較差,加鎖比較快,鎖衝突比較少,不太容易發生死鎖的情況。 -
MyISAM 會在磁盤上存儲三個文件,文件名和表名相同,擴展名分別是
.frm(存儲表定義)
、.MYD(MYData,存儲數據)
、MYI(MyIndex,存儲索引)
。這裏需要特別注意的是 MyISAM 只緩存索引文件
,並不緩存數據文件。 -
MyISAM 支持的索引類型有
全局索引(Full-Text)
、B-Tree 索引
、R-Tree 索引
Full-Text 索引:它的出現是爲了解決針對文本的模糊查詢效率較低的問題。
B-Tree 索引:所有的索引節點都按照平衡樹的數據結構來存儲,所有的索引數據節點都在葉節點
R-Tree索引:它的存儲方式和 B-Tree 索引有一些區別,主要設計用於存儲空間和多維數據的字段做索引,目前的 MySQL 版本僅支持 geometry 類型的字段作索引,相對於 BTREE,RTREE 的優勢在於範圍查找。
-
數據庫所在主機如果宕機,MyISAM 的數據文件容易損壞,而且難以恢復。
-
增刪改查性能方面:SELECT 性能較高,適用於查詢較多的情況
InnoDB 存儲引擎的特點
自從 MySQL 5.1 之後,默認的存儲引擎變成了 InnoDB 存儲引擎,相對於 MyISAM,InnoDB 存儲引擎有了較大的改變,它的主要特點是
-
支持事務操作,具有事務 ACID 隔離特性,默認的隔離級別是
可重複讀(repetable-read)
、通過MVCC(併發版本控制)來實現的。能夠解決髒讀
和不可重複讀
的問題。 -
InnoDB 支持外鍵操作。
-
InnoDB 默認的鎖粒度
行級鎖
,併發性能比較好,會發生死鎖的情況。 -
和 MyISAM 一樣的是,InnoDB 存儲引擎也有
.frm文件存儲表結構
定義,但是不同的是,InnoDB 的表數據與索引數據是存儲在一起的,都位於 B+ 數的葉子節點上,而 MyISAM 的表數據和索引數據是分開的。 -
InnoDB 有安全的日誌文件,這個日誌文件用於恢復因數據庫崩潰或其他情況導致的數據丟失問題,保證數據的一致性。
-
InnoDB 和 MyISAM 支持的索引類型相同,但具體實現因爲文件結構的不同有很大差異。
-
增刪改查性能方面,如果執行大量的增刪改操作,推薦使用 InnoDB 存儲引擎,它在刪除操作時是對行刪除,不會重建表。
MyISAM 和 InnoDB 存儲引擎的對比
-
鎖粒度方面
:由於鎖粒度不同,InnoDB 比 MyISAM 支持更高的併發;InnoDB 的鎖粒度爲行鎖、MyISAM 的鎖粒度爲表鎖、行鎖需要對每一行進行加鎖,所以鎖的開銷更大,但是能解決髒讀和不可重複讀的問題,相對來說也更容易發生死鎖 -
可恢復性上
:由於 InnoDB 是有事務日誌的,所以在產生由於數據庫崩潰等條件後,可以根據日誌文件進行恢復。而 MyISAM 則沒有事務日誌。 -
查詢性能上
:MyISAM 要優於 InnoDB,因爲 InnoDB 在查詢過程中,是需要維護數據緩存,而且查詢過程是先定位到行所在的數據塊,然後在從數據塊中定位到要查找的行;而 MyISAM 可以直接定位到數據所在的內存地址,可以直接找到數據。 -
表結構文件上
:MyISAM 的表結構文件包括:.frm(表結構定義),.MYI(索引),.MYD(數據);而 InnoDB 的表數據文件爲:.ibd和.frm(表結構定義);
主鍵索引和非主鍵索引的區別
參考:https://www.jianshu.com/p/f3a1e17a4df6
主鍵索引和非主鍵索引的區別是:非主鍵索引的葉子節點存放的是主鍵的值,而主鍵索引的葉子節點存放的是整行數據。非主鍵索引也被稱爲二級索引,而主鍵索引也被稱爲聚簇索引。
主鍵是邏輯鍵,索引是物理鍵,意思就是主鍵不實際存在,而索引實際存在在數據庫中
索引會真正的產生一個文件的
數據會真正的產生一個文件的
redo log 記錄的是物理日誌"某個數據頁上做了什麼修改" 循環使用
bin log 記錄的是邏輯日誌 語句的原始邏輯"ID=1 ,2 " 追加使用
最左前綴
在mysql建立聯合索引時會遵循最左前綴匹配的原則,即最左優先,在檢索數據時從聯合索引的最左邊開始匹配,示例:
對列col1、列col2和列col3建一個聯合索引
KEY test_col1_col2_col3 on test(col1,col2,col3);
聯合索引 test_col1_col2_col3 實際建立了(col1)、(col1,col2)、(col,col2,col3)三個索引。
SELECT * FROM test WHERE col1=“1” AND clo2=“2” AND clo4=“4”
上面這個查詢語句執行時會依照最左前綴匹配原則,檢索時會使用索引(col1,col2)進行數據匹配。
注意:索引的字段可以是任意順序的。
MYSQL MVCC
MVCC是一種多版本併發控制機制,大多數的MYSQL事務型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制.事實上,他們都和MVCC–多版本併發控制來一起使用.大家都應該知道,鎖機制可以控制併發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。
MVCC是通過保存數據在某個時間點的快照來實現的. 不同存儲引擎的MVCC. 不同存儲引擎的MVCC實現是不同的,典型的有樂觀併發控制和悲觀併發控制.
什麼是髒讀?幻讀?不可重複讀?Innodb怎麼解決幻讀?
髒讀:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據
不可重複讀:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果不一致。
幻讀:系統管理員A將數據庫中所有學生的成績從具體分數改爲ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。
RR 下,innodb下的幻讀是由MVCC 或者 GAP 鎖 或者是next-key lock 解決的。
MVCC判斷了記錄的可見性,比如 select count(*) from table where col_name = xxx 時(屬於快照讀),在RR 級別下,這條事務在事務一開始就生成了readview,通過這個readview 這條語句將會找到符合條件的行並且計算數量。 那麼關於與如何找到這些符合條件的行,滿足where 條件的同時也得滿。
參考:幻讀問題是指一個事務的兩次不同時間的相同查詢返回了不同的的結果集。例如:一個 select 語句執行了兩次,但是在第二次返回了第一次沒有返回的行,那麼這些行就是“phantom” row.read view(或者說 MVCC)實現了一致性不鎖定讀(Consistent Nonlocking Reads),從而避免了(非當前讀下)幻讀
計算機網絡
https和http
區別:HTTP + 加密 + 認證 + 完整性保護 = HTTPS
參考:https://www.jianshu.com/p/6c981b44293d
SSL/TLS 握手過程
參考:https://www.jianshu.com/p/7158568e4867
參考:https://imququ.com/post/optimize-tls-handshake.html
長連接短連接
參考:https://www.cnblogs.com/gotodsp/p/6366163.html
HTTP的長連接和短連接本質上是TCP長連接和短連接。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網絡層使用IP協議。 IP協議主要解決網絡路由和尋址問題,TCP協議主要解決如何在IP層之上可靠地傳遞數據包,使得網絡上接收端收到發送端所發出的所有包,並且順序與發送順序一致。TCP協議是可靠的、面向連接的。
客戶端和服務器每進行一次HTTP操作,就建立一次連接,任務結束就中斷連接。當客戶端瀏覽器訪問的某個HTML或其他類型的Web頁中包含有其他的Web資源(如JavaScript文件、圖像文件、CSS文件等),每遇到這樣一個Web資源,瀏覽器就會重新建立一個HTTP會話。而從HTTP/1.1起,默認使用長連接,用以保持連接特性。使用長連接的HTTP協議,
例子:
網上購物來說,HTTP協議是指的那個快遞單,你寄件的時候填的單子就像是發了一個HTTP請求,等貨物運到地方了,快遞員會根據你發的請求把貨物送給相應的收貨人。而TCP協議就是中間運貨的那個大貨車,也可能是火車或者飛機,但不管是什麼,它是負責運輸的,因此必須要有路,不管是地上還是天上。那麼這個路就是所謂的TCP連接,也就是一個雙向的數據通道。
tcp擁塞控制
參考:https://www.cnblogs.com/wuchanming/p/4422779.html
原因:網絡中的路由器會有一個數據包處理隊列,當路由器接收到的數據包太多而一下子處理不過來時,就會導致數據包處理隊列過長。此時,路由器就會無條件的丟棄新接收到的數據封包。
方法:慢開始與擁塞避免/快重傳和快恢復
1、慢開始
發送方維持一個叫做擁塞窗口cwnd(congestion window)的狀態變量。擁塞窗口的大小取決於網絡的擁塞程度,並且動態地在變化。發送方讓自己的發送窗口等於擁塞窗口,另外考慮到接受方的接收能力,發送窗口可能小於擁塞窗口。
慢開始算法的思路就是,不要一開始就發送大量的數據,先探測一下網絡的擁塞程度,也就是說由小到大逐漸增加擁塞窗口的大小。這裏用報文段的個數的擁塞窗口大小舉例說明慢開始算法,實時擁塞窗口大小是以字節爲單位的。
2、快重傳和快恢復
快重傳要求接收方在收到一個失序的報文段後就立即發出重複確認(爲的是使發送方及早知道有報文段沒有到達對方)而不要等到自己發送數據時捎帶確認。快重傳算法規定,發送方只要一連收到三個重複確認就應當立即重傳對方尚未收到的報文段,而不必繼續等待設置的重傳計時器時間到期。快重傳配合使用的還有快恢復算法,有以下兩個要點:
①當發送方連續收到三個重複確認時,就執行“乘法減小”算法,把ssthresh門限減半。但是接下去並不執行慢開始算法。
②考慮到如果網絡出現擁塞的話就不會收到好幾個重複的確認,所以發送方現在認爲網絡可能沒有出現擁塞。所以此時不執行慢開始算法,而是將cwnd設置爲ssthresh的大小,然後執行擁塞避免算法。如下圖:
UDP和TCP區別,分別怎麼處理丟包
TCP與UDP基本區別
1.基於連接與無連接
2.TCP要求系統資源較多,UDP較少;
3.UDP程序結構較簡單
4.流模式(TCP)與數據報模式(UDP);
5.TCP保證數據正確性,UDP可能丟包
6.TCP保證數據順序,UDP不保證
UDP應用場景:
1.面向數據報方式
2.網絡數據大多爲短消息
3.擁有大量Client
4.對數據安全性無特殊要求
5.網絡負擔非常重,但對響應速度要求高
具體編程時的區別
1.socket()的參數不同
2.UDP Server不需要調用listen和accept
3.UDP收發數據用sendto/recvfrom函數
4.TCP:地址信息在connect/accept時確定
5.UDP:在sendto/recvfrom函數中每次均 需指定地址信息
6.UDP:shutdown函數無效
TCP編程的服務器端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt(); * 可選
3、綁定IP地址、端口等信息到socket上,用函數bind();
4、開啓監聽,用函數listen();
5、接收客戶端上來的連接,用函數accept();
6、收發數據,用函數send()和recv(),或者read()和write();
7、關閉網絡連接;
8、關閉監聽;
與之對應的UDP編程步驟要簡單許多,分別如下:
UDP編程的服務器端一般步驟是:
1、創建一個socket,用函數socket();
2、設置socket屬性,用函數setsockopt();* 可選
3、綁定IP地址、端口等信息到socket上,用函數bind();
4、循環接收數據,用函數recvfrom();
5、關閉網絡連接;
TCP/UDP分別處理丟包方法
UDP不提供複雜的控制機制,利用IP提供面向無連接的通信服務。並且它是將應用程序發來的數據在收到的那一刻,立刻按照原樣發送到網絡上的一種機制。即使是出現網絡擁堵的情況下,UDP也無法進行流量控制等避免網絡擁塞的行爲。此外,傳輸途中如果出現了丟包,UDO也不負責重發。甚至當出現包的到達順序亂掉時也沒有糾正的功能。如果需要這些細節控制,那麼不得不交給由採用UDO的應用程序去處理。換句話說,UDP將部分控制轉移到應用程序去處理,自己卻只提供作爲傳輸層協議的最基本功能。UDP有點類似於用戶說什麼聽什麼的機制,但是需要用戶充分考慮好上層協議類型並製作相應的應用程序。
TCP充分實現了數據傳輸時各種控制功能,可以進行丟包的重發控制,還可以對次序亂掉的分包進行順序控制。而這些在UDP中都沒有。此外,TCP作爲一種面向有連接的協議,只有在確認通信對端存在時纔會發送數據,從而可以控制通信流量的浪費。TCP通過檢驗和、序列號、確認應答、重發控制、連接管理以及窗口控制等機制實現可靠性傳輸。
TCP/UDP應用場景
#TCP應用場景
當對網絡通信質量有要求時,比如:整個數據要準確無誤的傳遞給對方,這往往對於一些要求可靠的應用,比如HTTP,HTTPS,FTP等傳輸文件的協議,POP,SMTP等郵件的傳輸協議。常見使用TCP協議的應用:
1.瀏覽器使用的:HHTP
2.FlashFXP:FTP
3.Outlook:POP,SMTP
4.QQ文件傳輸
UDP 文件傳輸協議
對當前網絡通訊質量要求不高的時候,要求網絡通訊速度儘量的快,這時就使用UDP
日常生活中常見使用UDP協議:
1.QQ語音
2.QQ視頻
3.TFTP
UDP怎麼做簡單的可靠傳輸
參考:https://www.jianshu.com/p/6c73a4585eba
模仿傳輸層TCP的可靠性傳輸。下面不考慮擁塞處理,可靠UDP的簡單設計。
- 1、添加seq/ack機制,確保數據發送到對端
- 2、添加發送和接收緩衝區,主要是用戶超時重傳。
- 3、添加超時重傳機制。
詳細說明:送端發送數據時,生成一個隨機seq=x,然後每一片按照數據大小分配seq。數據到達接收端後接收端放入緩存,併發送一個ack=x的包,表示對方已經收到了數據。發送端收到了ack包後,刪除緩衝區對應的數據。時間到後,定時任務檢查是否需要重傳數據
七層協議/TCP/IP網絡模型
三次握手/四次揮手/三次握手屬於七層協議裏面的哪一層
三次握手
原因:爲了解決延遲SYN報文到達的問題。面試官覺得我沒答道點上,而是引導我思考如果第二次握手的SYN+ACK報文丟失了之後,客戶端會不斷髮送SYN報文,而由於沒有第三次握手,服務器端會認爲連接建立,不斷髮送數據。這時就產生了死鎖問題。這也是個不錯的角度,拓展了我的思路。
所謂的“三次握手”即對每次發送的數據量是怎樣跟蹤進行協商使數據段的發送和接收同步,根據所接收到的數據量而確定的數據確認數及數據發送、接收完畢後何時撤消聯繫,並建立虛連接。爲了提供可靠的傳送,TCP在發送新的數據之前,以特定的順序將數據包的序號,並需要這些包傳送給目標機之後的確認消息。TCP總是用來發送大批量的數據。當應用程序在收到數據後要做出確認時也要用到TCP。
在TCP/IP協議中,TCP協議提供可靠的連接服務,採用三次握手建立一個連接。
第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SENT狀態,等待服務器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。
第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
完成三次握手,客戶端與服務器開始傳送數據。
四次揮手
第一次揮手:客戶端給服務器發送TCP包,用來關閉客戶端到服務器的數據傳送。將標誌位FIN和ACK置爲1,序號爲X=1,確認序號爲Z=1。
第二次揮手:服務器收到FIN後,發回一個ACK(標誌位ACK=1),確認序號爲收到的序號加1,即X=X+1=2。序號爲收到的確認序號=Z。
第三次揮手:服務器關閉與客戶端的連接,發送一個FIN。標誌位FIN和ACK置爲1,序號爲Y=1,確認序號爲X=2。
第四次揮手:客戶端收到服務器發送的FIN之後,發回ACK確認(標誌位ACK=1),確認序號爲收到的序號加1,即Y+1=2。序號爲收到的確認序號X=2。
三次握手屬於傳輸層。
TCP 有哪些字段
1、ACK 是 TCP 報頭的控制位之一,對數據進行確認。確認由目的端發出, 用 它來告訴發送端這個序列號之前的數據段都收到了。 比如確認號爲 X,則表示 前 X-1 個數據段都收到了,只有當 ACK=1 時,確認號纔有效,當 ACK=0 時,確認 號無效,這時會要求重傳數據,保證數據的完整性。
2、SYN 同步序列號,TCP 建立連接時將這個位置 1。
3、FIN 發送端完成發送任務位,當 TCP 完成數據傳輸需要斷開時,,出斷開 連接的一方將這位置 1。
Socket 建立網絡連接的步驟:服務器監聽、客戶端請求、連接確認
爲什麼要三次握手和四次揮手?
TCP協議是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,採用全雙工通信。
那爲什麼需要三次握手呢?請看如下的過程:
- A向B發起建立連接請求:A——>B;
- B收到A的發送信號,並且向A發送確認信息:B——>A;
- A收到B的確認信號,並向B發送確認信號:A——>B。
三次握手大概就是這麼個過程。
通過第一次握手,B知道A能夠發送數據。通過第二次握手,A知道B能發送數據。結合第一次握手和第二次握手,A知道B能接收數據。結合第三次握手,B知道A能夠接收數據。
至此,完成了握手過程,A知道B能收能發,B知道A能收能發,通信連接至此建立。三次連接是保證可靠的最小握手次數,再多次握手也不能提高通信成功的概率,反而浪費資源。
那爲什麼需要四次揮手呢?請看如下過程:
- A向B發起請求,表示A沒有數據要發送了:A——>B;
- B向A發送信號,確認A的斷開請求請求:B——>A;
- B向A發送信號,請求斷開連接,表示B沒有數據要發送了:B——>A;
- A向B發送確認信號,同意斷開:A——>B。
B收到確認信號,斷開連接,而A在一段時間內沒收到B的信號,表明B已經斷開了,於是A也斷開了連接。至此,完成揮手過程。
可能有捧油會問,爲什麼2、3次揮手不能合在一次揮手中?那是因爲此時A雖然不再發送數據了,但是還可以接收數據,B可能還有數據要發送給A,所以兩次揮手不能合併爲一次。
揮手次數比握手多一次,是因爲握手過程,通信只需要處理連接。而揮手過程,通信需要處理數據+連接。
TIMEWAIT
參考:https://blog.csdn.net/u013616945/article/details/77510925
1. time_wait狀態如何產生?
由上面的變遷圖,首先調用close()發起主動關閉的一方,在發送最後一個ACK之後會進入time_wait的狀態,也就說該發送方會保持2MSL時間之後纔會回到初始狀態。MSL值得是數據包在網絡中的最大生存時間。產生這種結果使得這個TCP連接在2MSL連接等待期間,定義這個連接的四元組(客戶端IP地址和端口,服務端IP地址和端口號)不能被使用。
2.time_wait狀態產生的原因
1)爲實現TCP全雙工連接的可靠釋放
由TCP狀態變遷圖可知,假設發起主動關閉的一方(client)最後發送的ACK在網絡中丟失,由於TCP協議的重傳機制,執行被動關閉的一方(server)將會重發其FIN,在該FIN到達client之前,client必須維護這條連接狀態,也就說這條TCP連接所對應的資源(client方的local_ip,local_port)不能被立即釋放或重新分配,直到另一方重發的FIN達到之後,client重發ACK後,經過2MSL時間週期沒有再收到另一方的FIN之後,該TCP連接才能恢復初始的CLOSED狀態。如果主動關閉一方不維護這樣一個TIME_WAIT狀態,那麼當被動關閉一方重發的FIN到達時,主動關閉一方的TCP傳輸層會用RST包響應對方,這會被對方認爲是有錯誤發生,然而這事實上只是正常的關閉連接過程,並非異常。
2)爲使舊的數據包在網絡因過期而消失
爲說明這個問題,我們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP連接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我們先關閉,接着很快以相同的四元組建立一條新連接。本文前面介紹過,TCP連接由四元組唯一標識,因此,在我們假設的情況中,TCP協議棧是無法區分前後兩條TCP連接的不同的,在它看來,這根本就是同一條連接,中間先釋放再建立的過程對其來說是“感知”不到的。這樣就可能發生這樣的情況:前一條TCP連接由local peer發送的數據到達remote peer後,會被該remot peer的TCP傳輸層當做當前TCP連接的正常數據接收並向上傳遞至應用層(而事實上,在我們假設的場景下,這些舊數據到達remote peer前,舊連接已斷開且一條由相同四元組構成的新TCP連接已建立,因此,這些舊數據是不應該被向上傳遞至應用層的),從而引起數據錯亂進而導致各種無法預知的詭異現象。作爲一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種情況的發生,這正是TIME_WAIT狀態存在的第2個原因。
3)總結
具體而言,local peer主動調用close後,此時的TCP連接進入TIME_WAIT狀態,處於該狀態下的TCP連接不能立即以同樣的四元組建立新連接,即發起active close的那方佔用的local port在TIME_WAIT期間不能再被重新分配。由於TIME_WAIT狀態持續時間爲2MSL,這樣保證了舊TCP連接雙工鏈路中的舊數據包均因過期(超過MSL)而消失,此後,就可以用相同的四元組建立一條新連接而不會發生前後兩次連接數據錯亂的情況。
3.time_wait狀態如何避免
首先服務器可以設置SO_REUSEADDR套接字選項來通知內核,如果端口忙,但TCP連接位於TIME_WAIT狀態時可以重用端口。在一個非常有用的場景就是,如果你的服務器程序停止後想立即重啓,而新的套接字依舊希望使用同一端口,此時SO_REUSEADDR選項就可以避免TIME_WAIT狀態。
線程間同步的方式,進程間同步的方式和進程間通信的方式
瀏覽器打開一個網頁經歷了怎樣的過程
1.DNS域名解析:瀏覽器緩存、系統緩存、路由器、ISP的DNS服務器、根域名服務器。把域名轉化成IP地址。
2.與IP地址對應的服務器建立TCP連接,經歷三次握手:SYN,ACK、SYN,ACK
3.以get,post方式發送HTTP請求,get方式發送主機,用戶***,connection屬性,cookie等
4.獲得服務器的響應,顯示頁面
操作系統
死鎖
概念:當線程A持有獨佔鎖a,並嘗試去獲取獨佔鎖b的同時,線程B持有獨佔鎖b,並嘗試獲取獨佔鎖a的情況下,就會發生AB兩個線程由於互相持有對方需要的鎖,而發生的阻塞現象,我們稱爲死鎖。
死鎖怎麼解決?
造成死鎖必須達成的4個條件(原因):
- 互斥條件:一個資源每次只能被一個線程使用。
- 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:線程已獲得的資源,在未使用完之前,不能強行剝奪。
- 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關係。
方法:
- 互斥條件 ---> 獨佔鎖的特點之一。
- 請求與保持條件 ---> 獨佔鎖的特點之一,嘗試獲取鎖時並不會釋放已經持有的鎖
- 不剝奪條件 ---> 獨佔鎖的特點之一。
- 循環等待條件 ---> 唯一需要記憶的造成死鎖的條件。
進程、線程通信
參考:https://blog.csdn.net/J080624/article/details/87454764
進程間通信方式
分爲兩大類型:
- 低級通信,控制信息的通信(主要用於進程之間的同步,互斥,終止和掛起等等控制信息的傳遞)
- 高級通信,大批數據信息的通信(主要用於進程間數據塊數據的交換和共享,常見的高級通信有管道,消息隊列,共享內存等)。
IPC的方式通常有管道(包括無名管道和命名管道FIFO)、消息隊列(報文)、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
linux下的進程包含以下幾個關鍵要素:
-
有一段可執行程序;
-
有專用的系統堆棧空間;
-
內核中有它的控制塊(進程控制塊),描述進程所佔用的資源,這樣,進程才能接受內核的調度;
-
具有獨立的存儲空間
線程間通信
線程間的通信目的主要是用於線程同步,所以線程沒有像進程通信中的用於數據交換的通信機制。
① 鎖機制
互斥鎖、條件變量、讀寫鎖和自旋鎖。
互斥鎖確保同一時間只能有一個線程訪問共享資源。當鎖被佔用時試圖對其加鎖的線程都進入阻塞狀態(釋放CPU資源使其由運行狀態進入等待狀態)。當鎖釋放時哪個等待線程能獲得該鎖取決於內核的調度。
讀寫鎖當以寫模式加鎖而處於寫狀態時任何試圖加鎖的線程(不論是讀或寫)都阻塞,當以讀狀態模式加鎖而處於讀狀態時“讀”線程不阻塞,“寫”線程阻塞。讀模式共享,寫模式互斥。
條件變量可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。
自旋鎖上鎖受阻時線程不阻塞而是在循環中輪詢查看能否獲得該鎖,沒有線程的切換因而沒有切換開銷,不過對CPU的霸佔會導致CPU資源的浪費。 所以自旋鎖適用於並行結構(多個處理器)或者適用於鎖被持有時間短而不希望在線程切換產生開銷的情況。
② 信號量機制(Semaphore)
包括無名線程信號量和命名線程信號量。線程的信號和進程的信號量類似,使用線程的信號量可以高效地完成基於線程的資源計數。信號量實際上是一個非負的整數計數器,用來實現對公共資源的控制。在公共資源增加的時候,信號量就增加;公共資源減少的時候,信號量就減少;只有當信號量的值大於0的時候,才能訪問信號量所代表的公共資源。
參考博文:多線程併發之Semaphore(信號量)使用詳解
③ 信號機制(Signal)
類似進程間的信號處理。
④ violate全局變量-共享內存
參考:https://blog.csdn.net/J080624/article/details/85318075
例子:i++
線程進程協程
一、概念
進程:進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。每個進程都有自己的獨立內存空間,不同進程通過進程間通信來通信。由於進程比較重量,佔據獨立的內存,所以上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。
線程:線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
協程:協程是一種用戶態的輕量級線程,協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
二、區別:
1、進程多與線程比較
線程是指進程內的一個執行單元,也是進程內的可調度實體。線程與進程的區別:
1) 地址空間:線程是進程內的一個執行單元,進程內至少有一個線程,它們共享進程的地址空間,而進程有自己獨立的地址空間
2) 資源擁有:進程是資源分配和擁有的單位,同一個進程內的線程共享進程的資源
3) 線程是處理器調度的基本單位,但進程不是
4) 二者均可併發執行
5) 每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口,但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制
2、協程多與線程進行比較
1) 一個線程可以多個協程,一個進程也可以單獨擁有多個協程,這樣python中則能使用多核CPU。
2) 線程進程都是同步機制,而協程則是異步
3) 協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態
Select,epoll,epoll兩種觸發方式
參考:https://segmentfault.com/a/1190000003063859
I/O 多路複用就是我們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
Select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述副就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設爲null即可),函數返回。當select函數返回後,可以 通過遍歷fdset,來找到就緒的描述符。
select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點。select的一 個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般爲1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但 是這樣也會造成效率的降低。
epoll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都需要在返回後,
通過遍歷文件描述符來獲取已經就緒的socket
。事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。
epoll兩種觸發方式
epoll分爲兩種工作方式LT和ET。
LT(level triggered) 是默認/缺省的工作方式,同時支持 block和no_block socket。這種工作方式下,內核會通知你一個fd是否就緒,然後纔可以對這個就緒的fd進行I/O操作。就算你沒有任何操作,系統還是會繼續提示fd已經就緒,不過這種工作方式出錯會比較小,傳統的select/poll就是這種工作方式的代表。
ET(edge-triggered) 是高速工作方式,僅支持no_block socket,這種工作方式下,當fd從未就緒變爲就緒時,內核會通知fd已經就緒,並且內核認爲你知道該fd已經就緒,不會再次通知了,除非因爲某些操作導致fd就緒狀態發生變化。如果一直不對這個fd進行I/O操作,導致fd變爲未就緒時,內核同樣不會發送更多的通知,因爲only once。所以這種方式下,出錯率比較高,需要增加一些檢測程序。
LT可以理解爲水平觸發,只要有數據可以讀,不管怎樣都會通知。而ET爲邊緣觸發,只有狀態發生變化時纔會通知,可以理解爲電平變化。
線程池
參考:https://www.jianshu.com/p/b8197dd2934c
調用malloc時會立即分配物理內存嗎?頁表中一定會對應物理頁框嗎?swap交換空間
參考:https://blog.csdn.net/jasonLee_lijiaqi/article/details/79611293
malloc申請的就是虛擬內存,系統爲每個進程創建了一個頁表。在進程邏輯地址空間中的每一頁,依次在頁表中有一個表項,記錄了該頁對應的物理塊號。
Linux中Swap(即:交換分區),類似於Windows的虛擬內存,就是當內存不足的時候,把一部分硬盤空間虛擬成內存使用,從而解決內存容量不足的情況。
CPU高速緩存的邏輯
參考:https://www.cnblogs.com/dolphin0520/p/3749259.html
LRU:https://www.cnblogs.com/dolphin0520/p/3741519.html
最近最久未使用的意思。在操作系統的內存管理中,有一類很重要的算法就是內存頁面置換算法(包括FIFO,LRU,LFU等幾種常見頁面置換算法)。事實上,Cache算法和內存頁面置換算法的核心思想是一樣的:都是在給定一個限定大小的空間的前提下,設計一個原則如何來更新和訪問其中的元素。下面說一下LRU算法的核心思想,LRU算法的設計原則是:如果一個數據在最近一段時間沒有被訪問到,那麼在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。
而用什麼數據結構來實現LRU算法呢?可能大多數人都會想到:用一個數組來存儲數據,給每一個數據項標記一個訪問時間戳,每次插入新數據項的時候,先把數組中存在的數據項的時間戳自增,並將新數據項的時間戳置爲0並插入到數組中。每次訪問數組中的數據項的時候,將被訪問的數據項的時間戳置爲0。當數組空間已滿時,將時間戳最大的數據項淘汰。
優化做法:利用鏈表和hashmap。當需要插入新的數據項的時候,如果新數據項在鏈表中存在(一般稱爲命中),則把該節點移到鏈表頭部,如果不存在,則新建一個節點,放到鏈表頭部,若緩存滿了,則把鏈表最後一個節點刪除即可。在訪問數據的時候,如果數據項在鏈表中存在,則把該節點移到鏈表頭部,否則返回-1。這樣一來在鏈表尾部的節點就是最近最久未訪問的數據項。
經常使用的數據放在更高的緩存,如果在緩衝中沒找到,就用LRU這種緩存置換算法 (FIFO/LFU/LRU)
信號量與條件變量的區別
參考:https://www.cnblogs.com/charlesblc/p/6143397.html
互斥鎖實現的是線程之間的互斥,條件變量實現的是線程之間的同步。
對條件變量的理解:
1、條件變量與互斥鎖一樣,都是一種數據;
2、條件變量的作用是描述當前資源的狀態,即當前資源是否就緒。
3、條件變量是在多線程程序中用來實現“等待->喚醒”邏輯的常用方法。
對信號量的理解:
1.信號量是一種特殊的變量,它只能取自然數並且只支持兩種操作。
2.具有多個整數值的信號量稱爲通用信號量,只取 1 和 0 兩個數值的稱爲二元信號量,這裏我只討論二元信號量。
3.信號量的兩個操作:P(passeren,傳遞-進入臨界區)、V(vrijgeven,釋放-退出臨界區)
假設現有信號量sv,
P操作:如果sv的值大於0,就將其減1;如果sv的值爲0,就將當前線程掛起;
V操作:如果有其他線程因爲等待sv而被掛起,則將其喚醒;如果沒有,就將sv加1.
new和malloc的區別/delete和free的區別
與new和malloc差不多,C中malloc和free是一對,C++中new和delete是一對,delete先析構對象再回收內存,裏面會調用free
申請的內存所在位置:new操作符從自由存儲區(free store)上爲對象動態分配內存空間,而malloc函數從堆上動態分配內存。
返回類型安全性:new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。
內存分配失敗時的返回值:new內存分配失敗時,會拋出bac_alloc異常,它不會返回NULL;malloc分配內存失敗時返回NULL。
是否需要指定內存大小:使用new操作符申請內存分配時無須指定內存塊的大小,編譯器會根據類型信息自行計算,而malloc則需要顯式地指出所需內存的尺寸。
是否調用構造函數/析構函數:new/delete會調用對象的構造函數/析構函數以完成對象的構造/析構。而malloc則不會。
對數組的處理:C++提供了new[]與delete[]來專門處理數組類型: malloc得手動自定數組的大小
new與malloc是否可以相互調用:operator new /operator delete的實現可以基於malloc,而malloc的實現不可以去調用new。
是否可以被重載:opeartor new /operator delete可以被重載。而malloc/free並不允許重載。
能夠直觀地重新分配內存:使用malloc分配的內存後,如果在使用過程中發現內存不足,可以使用realloc函數進行內存重新分配實現內存的擴充,new沒有這個操作。
2、malloc需要注意什麼?
使用malloc申請內存空間需要了解:1)內存分配給誰?2)分配多大的內存?3)是否還有足夠內存分配?4)內存將用來存儲什麼格式的數據?5)分配的內存在哪裏?
使用malloc函數申請內存空間注意事項:1)內存是否申請成功? if( NULL !=p )2)使用結束後,一定要釋放,要求malloc和free符合一夫一妻制;3)內存釋放後(使用free函數之後指針變量p本身保存的地址並沒有改變),需要將p的值賦值爲NULL(拴住野指針)。