編程語言相關冷門知識:
1.++i與i++的效率問題:
萬年坑,如果不考慮編譯器本身的優化,對於內建的數據類型來說,二者效率是一樣的,只是在轉換成彙編語言時部分語句的執行順序不同罷了。如果是自定義類型(多是類、結構體),前綴自增++i是可以返回對象的引用的,但是後綴自增i++必須要返回對象的值,所以是需要進行復制後賦值的,會造成效率降低。前綴--和後綴--同++。
2.switch語句
switch語句的default接收的是除了已經寫出的case對應匹配之外的情況,而且所有的case、default語句之間沒有順序要求。
3.Java的內存
java的內存分爲5個部分:
1.棧:存放方法中的局部變量,當方法運行時,也需要壓入棧中,執行完成後彈出棧。
2.堆:實際數據存放的地方(new 出來的),並且會有賦默認值的行爲,整數(0),浮點數(0.0),字符('\u0000',爲Unicode字符),布爾(false),引用(null)。在堆中new出來的對象,他們在堆中會保存方法區中對應方法的地址值。[運行方法時,從棧(對象的引用)->堆(對象實際存放的地方)->方法區(找到方法區中該方法對應的相關信息)->棧(方法入棧,開始執行)]
3.方法區:存儲.class相關信息,主要是與方法相關的信息。
4.本地方法棧:與操作系統相關。
5.寄存器:與CPU相關,我們可以通過JAVA使用CPU上的一些寄存器。
具體內存分配過程:例如,當我們在一個名叫main的方法中創建了一個數組:int a[]=new int[10]。程序開始時,所有類的字節碼文件(.class)都會加載到方法區,當方法執行時,會在棧中開闢一段空間給方法,在棧空間中建立引用a,然後在堆中找一塊區域生成長度爲10的數組,並將起始地址給引用a。
注意:對於數組的靜態初始化int[] a={1,3,4}來說,本身是個簡略寫法,完整寫法是int[] a=new int[]{1,3,4},所以依舊是在堆中開闢內存,由於是在堆中存放數據,因此還是會默認初始化,只是初始化後再賦值一次。
注意:在Java中,基本數據類型的變量存儲的是值,引用類型存儲的是地址值。
個人發現Java的char類型採用utf-8的方式實現存儲多種字符,即遇到擁有字節不夠表示需要存儲的字符時,一次增加8位即1字節的方式,直到可以存儲爲止。
4.Java的導包
處在同一個包下的類可以不顯示地使用import關鍵字導入便使用。
java.lang包下的類無需顯式導包便可以使用。
5.Java的局部變量與成員變量
6.Java Bean
就是一個標準的java類。標準規則爲:
1.所有的成員變量必須用private修飾,只能在類中進行訪問
2.每一個成員變量擁有一對Getter/Setter方法
3.無參構造和全部參數構造
7.Random使用注意
Random.nextInt()的區間爲整個Int的範圍[包含負數部分]。而Random.nextInt(x)的區間爲[0,x)。
8.泛型<E>
一開始並不知道使用者會選擇使用什麼類型,但是所有方法都通用的時候,可以使用泛型,使得可以在創建這個類的對象時再指定使用的具體類型。需要注意的時,泛型說只能是引用類型,所以對於基本類型來說,需要使用其包裝類(從JDK1.5開始,支持自動裝箱(基本類型->包裝類)與自動拆箱(包裝類->基本類型))。
9.String類型詳解以及注意事項
String類的常用構造:
1.public String():創建空的字符串。
2.public String(char[] array):根據字符數組的內容創建對應字符串。
3.public String(byte[] array):根據字節數據內容創建對應字符串。
4.String str="字符串內容":直接創建,這種方式創建的字符串是可共享的常量。
注意事項:
1.String是常量,所以一旦創建後就不可改變(所以凡是設計到改變字符串的行爲,無論是+還是String.concat、replace等方法都是創建了一個新的String,包括對String的引用進行賦值,只是改變了目標字節數組的地址值罷了)
2.數據相同的String其實是共享一段內存的
3.效果上相當於字符數組,實際底層是byte[]字節數組(具體效果由解碼方式決定)
4.String的==做的是地址的比較,做內容的比較需要調用String.equals(Object object)方法(很容易踩的坑),嚴格比較內容,大小寫敏感,另外推薦使用"字符串內容".equals(String str),而不推薦str.equals("字符串內容")。爲了避免str==null時調用方法導致空指針異常。
5.String.length()方法返回的長度是String的字符個數,但是並不一定等於底層字節數組的長度,尤其是當String裏融合了其他非英文字符的時候。String也可以使用equalsIgnoreCase(String)方法進行忽略(英文字母)大小寫的比較【別想着中文一壹的大小寫。。】
6.String.split(String regex)用於切割字符串,但是要注意傳入的參數是正則表達式,對於在正則表達式中有特殊含義的符號如'.',需要使用"\\."來表示。
String的hashcode(對應虛擬機佔有的程序內存位置)方法源碼:
在堆內存中,有一塊區域叫做字符串常量池,裏面存儲了字符串對象的地址值,指向一個字節數組(String的底層實現)。所有直接顯示寫出來的字符串量均會用字符串常量池維護,以便於共享,但是,如果是採用new操作,即使字符串常量池中有和他相同的值,也不是同一個對象,而是在堆內存中另找一塊存儲了對應的字節數組,並且在堆中用一個String引用存儲該地址值,賦值時把引用對應的地址值賦值出去。
10.static關鍵字
static靜態,用於修飾一些跟着類而不是對象走的屬性或方法,一般直接類.屬性或者類.方法的方式使用,當然也可以new一個對象出來,用對象.的方法使用,但是並不推薦,而且這種寫法在編譯後也會被javac修改爲類.。在本類的其他方法中調用該類中的靜態方法時,可以直接調用而不寫類名,javac會自動修改。
注意:
1.靜態是不能訪問非靜態的。因爲在內存中是先產生靜態內容,後產生非靜態內容。
2.靜態方法中不能使用this關鍵字。因爲this指向的是對象,誰調用當前的方法,誰就是這個對象,但是靜態是跟着類走的。
內存使用:static修飾的內容會存儲在方法區中的靜態區裏,向外提供地址。
靜態代碼塊:在類中寫的一段用static修飾的代碼塊,當第一次本類被使用時,執行唯一的一次。並且靜態內容總是優先於非靜態內容執行,因此可以得知靜態代碼塊甚至比構造方法執行,並只會執行一次。一般用於對靜態成員變量進行賦值。
public demo{
static{
//靜態代碼塊內容
}
}
11.Java常用工具類及方法
java.util.Arrays:與數組相關的工具類,提供大量操作數組的靜態方法
Arrays.toString(數組)將數組中的數據按照[元素1,元素2..]的方式格式化輸出
Arrays.sort(數組)將數據升序排序
java.util.Math:與數學操作相關的工具類。
Math.abs(double num):絕對值
Math.ceil(double num):上取整
Math.floor(double num):下取整
public static long Math.round(double num):四捨五入成Long型整數
Math.PI:api中提供的圓周率
其實可以採用強轉的方式直接捨棄掉小數部分。
面試數據結構篇:
難度中等89
給定一個單詞列表,我們將這個列表編碼成一個索引字符串 S
與一個索引列表 A
。
例如,如果這個列表是 ["time", "me", "bell"]
,我們就可以將其表示爲 S = "time#bell#"
和 indexes = [0, 2, 5]
。
對於每一個索引,我們可以通過從字符串 S
中索引的位置開始讀取字符串,直到 "#" 結束,來恢復我們之前的單詞列表。
那麼成功對給定單詞列表進行編碼的最小字符串長度是多少呢?
示例:
輸入: words =["time", "me", "bell"]
輸出: 10 說明: S ="time#bell#" , indexes = [0, 2, 5
] 。
提示:
1 <= words.length <= 2000
1 <= words[i].length <= 7
- 每個單詞都是小寫字母 。
思路:讀懂題意後不難發現,這道題考察的是串的後綴問題,只要A是B的後綴串,那麼只用B#就可以同時表示B和A。可以直接用O(N^2)的方法去遍歷進行後綴匹配,當然也有高效的數據結構Tire樹。下面是Tire樹的做法:將所有的串進行翻轉後建成Tire樹,就可以達到需要的求後綴串效果,最後統計一下到達所有葉子節點的路徑長度即可,因爲到葉子說明這就是上述舉例說明的B,也就是必須要存儲的串,同時因爲還需要一個'#'分隔,對長度加上1即可,至於爲什麼不直接在建樹的時候把高度也就是路徑長度存儲爲加1呢,其實也可以,但是要注意高度爲1的情況其實對應的是root節點,root節點爲根節點表示的其實是沒有任何串,此使應該返回的是0而不是路徑長度,這個就看個人選擇了。
class Solution {
public:
struct Node{
int height=0,childSum=0;//統計孩子個數 爲0則是葉子
Node* next[26]={NULL};
Node(){
}
Node(int height){
this->height=height;
}
};
queue<Node*> que;
int BFS(Node *root){
int ans=0,i;//長串的數量 長串的長度和
que.push(root);
Node *temp;
while(!que.empty()){
temp=que.front();
que.pop();
if(temp->childSum==0){
ans+=temp->height+1;
}
else{
for(i=0;i<26;++i){
if(temp->next[i]){
que.push(temp->next[i]);
}
}
}
}
return ans;
}
//子串必須是她的後綴串 所有字符串逆序 用Tire樹建立 返回所有到葉子的長度以及個數 再加上#
int minimumLengthEncoding(vector<string>& words) {
int n=words.size(),i,len,j;
string str;
Node *root=new Node(0),*p;
for(i=0;i<n;++i){//建成Tire樹
str=words[i];
reverse(str.begin(),str.end());
len=str.length();
p=root;
for(j=0;j<len;++j){
if(!(p->next[str[j]-'a'])){
p->next[str[j]-'a']=new Node(p->height+1);
}
++(p->childSum);
p=p->next[str[j]-'a'];
}
}
return BFS(root);
}
};
面試算法題:
難度簡單12
請完成一個函數,輸入一個二叉樹,該函數輸出它的鏡像。
例如輸入:
4
/ \
2 7
/ \ / \
1 3 6 9
鏡像輸出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
輸入:root = [4,2,7,1,3,6,9]
輸出:[4,7,2,9,6,3,1]
限制:
0 <= 節點個數 <= 1000
思路:遞歸題。有兩種做法,一種是生成法,一種是直接原地操作,用遞歸返回子樹的根節點。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(!root)return NULL;
TreeNode* mirror=new TreeNode(root->val);
DFS(root->left,mirror->right);
DFS(root->right,mirror->left);
return mirror;
}
void DFS(TreeNode* &root,TreeNode* &mirror){
if(!root)return;
mirror=new TreeNode(root->val);
DFS(root->left,mirror->right);
DFS(root->right,mirror->left);
}
};
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(!root)return NULL;
TreeNode *left=mirrorTree(root->right);
TreeNode *right=mirrorTree(root->left);
root->left=left;
root->right=right;
return root;
}
};
難度中等45
輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷結果。如果是則返回 true
,否則返回 false
。假設輸入的數組的任意兩個數字都互不相同。
參考以下這顆二叉搜索樹:
5
/ \
2 6
/ \
1 3
示例 1:
輸入: [1,6,3,2,5]
輸出: false
示例 2:
輸入: [1,3,2,6,5]
輸出: true
提示:
數組長度 <= 1000
思路:一顆二叉搜索樹的性質爲,左子樹的所有結點小於當前結點,右子樹的所有結點大於當前節點,因此二叉搜索樹的後序遍歷(左右中)則表現爲最終的結果是 < > =,因此每次等於的值是根的值,保證左邊全部小於,右邊全部大於,然後遞歸進入子樹判斷是否是二叉搜索樹即可,由於每層需要遍歷的數組長度爲n,一顆二叉搜索樹最壞情況的高度爲n,因此時間複雜度達到了O(n^2),這題是能過掉的。
class Solution {
public:
bool isOk(vector<int>& postorder,int L,int R){
if(L>=R)return true;
int M=L-1;
bool haveMore=false;
for(int i=L;i<=R;++i){
if(postorder[i]<postorder[R]){
if(haveMore){
return false;
}
else M=i;
}else{
haveMore=true;
}
}
return isOk(postorder,L,M)&&isOk(postorder,M+1,R-1);
}
bool verifyPostorder(vector<int>& postorder) {
return isOk(postorder,0,postorder.size()-1);
}
};
難度簡單7
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。
你可以假設數組是非空的,並且給定的數組總是存在多數元素。
示例 1:
輸入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
輸出: 2
限制:
1 <= 數組長度 <= 50000
思路:題意爲有一個數字出現的次數超過數組長度的一般,採用消去法,每一個數與和他不同的數進行消去,由於目標數字出現了超過一半的次數,即使讓他和其餘所有的數進行消去,他都會至少多出一個。
class Solution {
public:
int majorityElement(vector<int>& nums) {
int len=nums.size(),i,count=0,ans;
ans=nums[0];
count=1;
for(i=1;i<len;++i){
if(!count){
ans=nums[i];
count=1;
continue;
}
if(nums[i]!=ans)--count;
else ++count;
}
return ans;
}
};
改進題:目標在數組中出現的次數爲數組長度的一半,數組長度爲偶數,求目標數字。
思路:我們先刨去最後一個數字不看:
A.假設最後一個數字不是目標數,那麼前面的n-1個數字中有n/2個是目標數,用消去法則會剩下目標數字
B.假設最後一個就是目標數
綜上,答案只可能是前n-1個數中出現最多的數或是最後一個數。
class Solution {
public:
int majorityElement(vector<int>& nums) {
int len=nums.size(),i,count=0,ans,tar=nums[len-1],assume=1;
ans=nums[0];
count=1;
for(i=1;i<len-1;++i){
if(tar==nums[i])++assume;
if(!count){
ans=nums[i];
count=1;
continue;
}
if(nums[i]!=ans)--count;
else ++count;
}
return assume==(len>>1)?tar:ans;
}
};
難度簡單84
輸入整數數組 arr
,找出其中最小的 k
個數。例如,輸入4、5、1、6、2、7、3、8這8個數字,則最小的4個數字是1、2、3、4。
示例 1:
輸入:arr = [3,2,1], k = 2 輸出:[1,2] 或者 [2,1]
示例 2:
輸入:arr = [0,1,2,1], k = 1 輸出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
思路:直接的做法就是排序後,直接取出前k個即可。但是可以使用荷蘭國旗問題優化,我們知道對於荷蘭國旗問題來說,一次paration過程後,數組會變成如下情況:
當k落在[l,r-1]的時候,可以保證[1,l]一定是最小的k個數,其他情況都需要再次計算。
class Solution {
void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
int[] paration(int[] arr,int L,int R,int target){//使用荷蘭國旗問題
int l=L-1,r=R+1,i=L;
while(i<r){
if(arr[i]<target){
swap(arr,i++,++l);
}else if(arr[i]>target){
swap(arr,i,--r);
}else{
++i;
}
}
return new int[]{l+1,r-1};//確保 [0,l] < target [l+1,r-1]==target
}
void solve(int[] arr,int L,int R,int K){//在[L,R]中找到分割點
//int target=(int)Math.random()*(R-L+1)+L;
//System.out.print("本次選取的值爲:"+arr[L+((R-L)>>1)]+" ");
int[] p=paration(arr,L,R,arr[L+((R-L)>>1)]);
//for(int i=0;i<arr.length;++i){
//System.out.print(arr[i]+" ");
//}
//System.out.println();
if(K<p[0]-1){
solve(arr,L,p[0]-1,K);
}else if(K>p[1]){
solve(arr,p[1]+1,R,K);
}else return;
}
public int[] getLeastNumbers(int[] arr, int k) {
if(arr.length==k)return arr;
//for(int i=0;i<arr.length;++i){
//System.out.print(arr[i]+" ");
//}
//System.out.println();
solve(arr,0,arr.length-1,k-1);
int[] ans=new int[k];
for(int i=0;i<k;++i){
ans[i]=arr[i];
}
return ans;
}
}
難度簡單
輸入一個整型數組,數組裏有正數也有負數。數組中的一個或連續多個整數組成一個子數組。求所有子數組的和的最大值。
要求時間複雜度爲O(n)。
示例1:
輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
思路:抽象後的題意爲,在給定數組中,找到一個區間[L,R],不存在區間內元素和比他更大的區間,返回這個和。
思路1:直接枚舉L、R,計算區間和,n^3複雜度
思路2:區間和可以用前綴和數組來優化,枚舉L、R,複雜度降低爲n^2
思路3:上述思路複雜度高是因爲存在兩個變量,所以我們需要考慮是否能夠固定住其中一個變量。顯然是可以的,我們設dp[i]表示以第i個元素作爲選定區間右邊界的情況下能得到的最大區間和,那麼dp[i]=max(dp[i+1]+nums[i],nums[i]),第一種是把第i個元素放入以第i-1個元素結尾的區間的最大和,第二種是以第i個元素作爲新區間和左邊界的情況,直接線性掃描,更新值即可。
class Solution {
public:
//dp[i] 第i位結束的序列和最大值
int maxSubArray(vector<int>& nums) {
int i,len=nums.size(),Ans=nums[0];
for(i=1;i<len;++i){
nums[i]=max(nums[i-1]+nums[i],nums[i]);
Ans=max(Ans,nums[i]);
}
return Ans;
}
};
一個整型數組 nums
裏除兩個數字之外,其他數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間複雜度是O(n),空間複雜度是O(1)。
示例 1:
輸入:nums = [4,1,4,6]
輸出:[1,6] 或 [6,1]
示例 2:
輸入:nums = [1,2,10,4,1,4,3,3]
輸出:[2,10] 或 [10,2]
限制:
2 <= nums <= 10000
思路:首先從類似的題目,一個數出現一次,另外的數全部都出現兩次,用異或運算解。我們可以借鑑其思路,進行轉化。本題是有兩個只出現一次的數字,我們可以將原數組按照某種規則分割成兩個只有一個數出現一次的數組,在對他們直接異或就是答案了。
規則:我們將所有的數組進行異或,最後的結果一定是兩個不同數的抑或結果,那麼他們的二進制中肯定有1,我們取出這個'1',按照數字有沒有這個1進行分組,因爲0^1=1,所以按照這個規則下,兩個只出現一次的數字一定不會在同一個數組裏,至於其他的數出現在哪個數組根本不需要關心,因爲x^x=0,並且不可能發生同一個數組同一個規則卻分到兩個數組裏去的情況。
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
int ans1=0,ans2=0,len=nums.size(),i,standard=0;
vector<int> ans;
for(i=0;i<len;++i)standard^=nums[i];
standard=standard&(-standard);
for(i=0;i<len;++i){
if(nums[i]&standard){
ans1^=nums[i];
}
else{
ans2^=nums[i];
}
}
ans.push_back(ans1);
ans.push_back(ans2);
return ans;
}
};
難度簡單
給定一個數組 A[0,1,…,n-1]
,請構建一個數組 B[0,1,…,n-1]
,其中 B
中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]
。不能使用除法。
示例:
輸入: [1,2,3,4,5]
輸出: [120,60,40,30,24]
提示:
- 所有元素乘積之和不會溢出 32 位整數
a.length <= 100000
思路:題意抽象就是B[i]爲A[i]左邊乘積和A[i]右邊乘積相乘後的結果,顯而易見的思路是二重循環,運算結果。
優化方法:Left[i]=Left[0]*Left[1]*...*Left[i-1],Right[i]=Right[i+1]*...*Right[n-1],於是ans[i]=Left[i]*Right[i]。
class Solution {
public:
int Left[100005],Right[100005];
//Left[i]:0~i-1的乘積 Right[i]:i+1~n-1的乘積
vector<int> constructArr(vector<int>& a) {
vector<int> ans;
int n=a.size(),i;
Left[0]=Right[n-1]=1;
for(i=1;i<n-1;++i){
Left[i]=a[i-1]*Left[i-1];
Right[n-1-i]=Right[n-i]*a[n-i];
}
if(n>1){
Right[0]=a[1]*Right[1];
Left[n-1]=Left[n-2]*a[n-2];
}
for(i=0;i<n;++i){
ans.push_back(Left[i]*Right[i]);
}
return ans;
}
};
空間優化版:
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
int n=a.size(),i,temp;
vector<int> ans(n,1);
temp=1;
for(i=0;i<n;++i){
ans[i]=temp;
temp*=a[i];
}
temp=1;
for(i=n-1;i>=0;--i){
ans[i]*=temp;
temp*=a[i];
}
return ans;
}
};