1. 程式人生 > 其它 >位元組跳動Java崗6月9號一面經驗分享,是真的有難度

位元組跳動Java崗6月9號一面經驗分享,是真的有難度

前言


這篇面經來自一位粉絲的投稿,他在前天也就是6月9號通過了位元組跳動的一面,分享了一些面試遇到的問題,然後我整理了出來,希望對即將參加面試的你們有一些幫助。

我們先來看一下他被問了哪些問題

  • 自我介紹
  • 看你簡歷上寫了死鎖,簡單介紹一下? 更改你的程式碼,使死鎖解除?
  • 講講系統呼叫?
  • 程序執行緒區別?
  • 程序通訊幾種方式?哪種方式最快?為什麼?
  • 程序怎麼分配的?PCB裡面都有什麼?為什麼程序上下文切換消耗大量資源?
  • 程序排程演算法?
  • 剛剛你說到零拷貝,簡單說一下過程?
  • 談談你常用的資料結構
  • 你都知道哪些樹結構?說說他們特性?(搜尋樹、平衡樹、紅黑樹、B樹、B+、霍夫曼)
  • 細講紅黑樹
  • 霍夫曼樹演算法貪心策略證明。
  • 剛剛你說到棧和佇列可以互相轉換,寫出程式碼。
  • 說說常見的排序演算法
  • 快排缺點?怎麼避免?手撕上面3個演算法?

除此之外還有兩個反問:

  • 我還有什麼需要增強的?
  • 如果可以參加專案開發,我還需要學習什麼新技術?

除了這些之外我還整理了一些別的大廠面試真題

可以分享給大家,需要的朋友轉發本文+關注+私信【611】就可以領取了

題解

好,現在我們一道一道的來看問的這些問題

1、自我介紹


面試自我介紹雖然人人都準備,但是做到讓人印象深刻可不容易啊,甚至有的人壓根都不重視,只想靠技術折服面試官,當然了,如果真的是很牛的技術大牛,那應該是沒問題。

面試是什麼?

它是個機會,讓面試官更進一步確認你是他們需要的人,你進一步展現你的交際溝通能力。

So

一定一定要記住,自己是在跟人打交道,而不是對機器答問題!

首先,為啥面試官要提這個問題呢?簡歷裡面不是寫得好清楚好詳細了嗎?

其實呢,這個問題,據很多做面試官的朋友所說,要是給面試官一個緩衝的時間來重新熟悉你的簡歷

所以,我們要通過自我介紹提醒面試官,你的特點和你為什麼特別合適這個職位。當然了,語言儘量簡練,諮詢了一些資深面試官後我總結了面試的3個要點,給大家參考一下。

時間控制在1分鐘,寫在紙上就是120-160個字:

  • 我是誰
  • 我的三個亮點,最近最相關的經歷?
  • 我為什麼想要這份工作?

這三個點用精煉的語言表達清楚自我介紹這一塊基本就沒什麼問題了。

2、死鎖怎麼解決

解決死鎖一般有四個階段,即

  • 死鎖預防
  • 死鎖避免
  • 死鎖檢測
  • 死鎖解除

死鎖預防: 破壞導致死鎖必要條件中的任意一個就可以預防死鎖。

例如:
使用銀行家演算法:指在分配資源之前先看清楚,資源分配後是否會導致系統死鎖。如果會死鎖,則不分配,否則就分配。

死鎖檢測: 判斷系統是否屬於死鎖的狀態,如果是,則執行死鎖解除策略。

死鎖解除: 將某程序所佔資源進行強制回收,然後分配給其他程序。(與死鎖檢測結合使用的)

3、系統呼叫

在我們執行的使用者程式中,凡是與系統級別的資源有關的操作(例如檔案管理、程序控制、記憶體管理等)都必須通過系統呼叫方式向OS提出服務請求,並由OS代為完成

平常我們的程序幾乎都是使用者態,讀取使用者資料,當涉及到系統操作,計算機資源的時候就要用到系統呼叫了

系統呼叫的功能大致分為

系統呼叫的功能與其作用一樣——涉及計算機資源的操作

  • 裝置管理:完成裝置的請求/釋放以及裝置的啟動
  • 檔案管理:完成檔案的讀寫、刪除、建立等功能
  • 程序控制:完成程序的建立、撤銷、阻塞以及喚醒等功能
  • 記憶體管理:完成記憶體的分配、回收以及獲取作業佔用記憶體區大小和地址等功能

4、程序執行緒的區別

程序是一個在記憶體中執行的應用程式。每個程序都有自己獨立的一塊記憶體空間,一個程序可以有多個執行緒,比如在Windows系統中,一個執行的xx.exe就是一個程序。

執行緒是程序中的一個執行任務(控制單元),負責當前程序中程式的執行。一個程序至少有一個執行緒,一個程序可以執行多個執行緒,多個執行緒可共享資料。

與程序不同的是同類的多個執行緒共享程序的堆和方法區資源,但每個執行緒有自己的程式計數器、虛擬機器棧和本地方法棧,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程序小得多,也正因為如此,執行緒也被稱為輕量級程序。

5、程序通訊的幾種方式

  • 無名管道( pipe):管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程序間使用。程序的親緣關係通常是指父子程序關係。
  • 高階管道(popen):將另一個程式當做一個新的程序在當前程式程序中啟動,則它算是當前程式的子程序,這種方式我們成為高階管道方式。
  • 有名管道 (named pipe) : 有名管道也是半雙工的通訊方式,但是它允許無親緣關係程序間的通訊。
  • 訊息佇列( message queue ) :訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
  • 訊號量( semophore ) :訊號量是一個計數器,可以用來控制多個程序對共享資源的訪問。它常作為一種鎖機制,防止某程序正在訪問共享資源時,其他程序也訪問該資源。因此,主要作為程序間以及同一程序內不同執行緒之間的同步手段。
  • 訊號 ( sinal ) : 訊號是一種比較複雜的通訊方式,用於通知接收程序某個事件已經發生。
  • 共享記憶體( shared memory ):共享記憶體就是對映一段能被其他程序所訪問的記憶體,這段共享記憶體由一個程序建立,但多個程序都可以訪問。共享記憶體是最快的 IPC方式,它是針對其他程序間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如訊號兩,配合使用,來實現程序間的同步和通訊。
  • 套接字( socket ) : 套介面也是一種程序間通訊機制,與其他通訊機制不同的是,它可用於不同機器間的程序通訊。

6、程序排程演算法

基於程序排程的兩種方式的排程演算法如下:

  • 先來先服務(FCFS)排程演算法
  • 短作業優先(SJF)排程演算法
  • 時間片輪轉(RR)排程演算法
  • 最高優先順序優先排程演算法
  • 多級反饋佇列(MFQ)排程演算法

這裡篇幅所限,就只講一下前面兩種吧,其餘的如果感興趣可以自己找資料看看

先來先服務(FCFS)排程演算法

1、簡介:先來先服務排程演算法是一種最簡單的排程演算法,可用於作業排程,也可用於程序排程。

2、原理:在程序排程中採用先來先服務演算法的時候,每次排程就從就緒佇列中選一個最先進入該佇列的程序,為之分配處理機,即誰第一排隊誰就先被執行。

3、優點:

有利於長作業(程序)

有利於CPU繁忙型的作業(程序)

4、缺點:

不利於短作業(程序)

不利於I/O繁忙型的作業(程序)

短作業優先(SJF)排程演算法

1、簡介:短作業(程序)優先排程演算法是指短作業或者短程序的優先排程演算法,它們分別作用於作業排程和程序排程,它是先來先服務排程演算法的一種優化版本。

2、原理:短程序優先排程演算法是從就緒佇列中選出一個估計執行時間最短的程序,再將處理機分配給它,直到執行完成,而其他程序一般不搶先正在執行的程序。

3、優點:

演算法對長作業(程序)不利(長作業(程序)長期不被排程)

未考慮程序的緊迫程度

由於是估計執行時間而定,而這個時間是由使用者所提供的,所以該演算法不一定能真正做到短作業優先排程

7、零拷貝過程

這裡給你們看張圖應該就是明白了,面試的時候如果遇到這道題,用自己的話說出來即可

8、常用的資料結構

這個你麼根據自己的使用情況實話實說就可以了,陣列,連結串列,佇列,棧,樹,Hash表這些,隨便說個兩三個就行。

9、霍夫曼樹演算法貪心策略證明

如何理解貪心演算法?

假設我們有一個可以容納 100kg 物品的揹包,可以裝以下 5 種豆子,每種豆子的總量和總價值各不相同,為了使揹包中所裝物品的總價值最大,我們如何選擇在揹包中裝哪些豆子?每種豆子又該裝多少呢?

實際上,我們只要先算一算每個物品的單價,按照單價由高到低來裝就好了。依次是黑豆、綠豆、紅豆、青豆、黃豆,所以我們可以往揹包裡裝 20kg 黑豆、30kg 綠豆、50kg 紅豆。

這個問題的解決思路本質上藉助的就是貪心演算法。結合這個例子,總結了一下貪心演算法解決問題的步驟:

第一步、

當我們看到這類問題的時候,首先應該聯想到貪心演算法:針對一組資料,我們定義了限制值和期望值,希望從中選出幾個資料,在滿足限制的情況下,期望值最大。類比剛才的例子,限制值就是總量不能超過 100kg,期望值就是物品的總價值,這組資料就是 5 種豆子。

第二步、

我們嘗試看下這個問題是否可以用貪心演算法解決:每次選擇當前情況下,在對限制值同等貢獻量的情況,對期望值貢獻最大的資料。類比剛才的例子,就是每次從剩下的豆子裡面,選擇單價最高的,也就是重量相同的情況下,對價值貢獻最大的豆子。

第三步、

實際上,貪心演算法解決問題的思路,並不總能給出最優解。

舉個例子,在一個有權圖中,我們從頂點 S 出發,找一條到頂點 T 的最短路徑(路徑中邊的權值總和最小)。貪心演算法的結局思路是,每次選擇都選擇一條跟當前頂點相連的圈最小的邊,直到找到頂點 T,按照這種思路,找出最短路徑:S->A->E->T ,路徑長度是 1 + 4 + 4 = 9。顯然不是最短的,因為最短的是:S->B->D->T,總長度是 2 + 2 + 2 = 6

為什麼貪心演算法在這個問題上不工作了呢,主要原因是,前面的選擇會影響後面的選擇。所以即便我們第一步選擇最優的走法,但有可能因為這一步選擇,導致後面每一次的選擇都很糟糕,最終也自然和最優解無緣了。

霍夫曼編碼

接下來看看霍夫曼編碼是如何利用貪心演算法來實現對資料壓縮編碼,有效節省資料儲存空間的

假設我有一個包含 1000 個字元的檔案,每個字元佔 1 個 byte (1 byte = 8 bits),儲存這 1000 個字元就一共需要 8000 bits,那麼有沒有更加節省空間的儲存方式呢?

假設我們統計發現,這 1000 個字元只包含 6 種不同字元,假設它們分別是 a、b、c、d、e、f。而 3 個二進位制 bit 就可以表示8 個不同的字元,所以為了儘量減少儲存空間的儲存空間,每個字元我們用 3 個二進位制位來表示,比如:a(000)、b(001)、c(010)、d(011)、e(100)、f(101),那麼儲存這 1000 個字元只需要 3000bits 就可以了,比原來的儲存節省了很多空間,那麼還有沒有更加節省空間的儲存方式呢?

這個時候就要用到霍夫曼編碼了。霍夫曼編碼是一種十分有效的編碼方法,廣泛用於資料壓縮中,其壓縮率通常在 20%~90% 之間。

霍夫曼編碼不僅會考察文字中有多少個不同字元,還會考察每個字元出現的頻率,根據頻率的不同,選擇不同長度的編碼。霍夫曼編碼試圖用這種不等長的編碼方法,來進一步增加壓縮的效率。

如何給不同頻率的字元選擇不同長度的編碼呢,根據貪心的思想,我們可以把出現頻率比較多的字元,用稍微短一些的編碼;出現頻率比較少的字元,用稍微長一些的編碼。

對於等長的編碼來說,壓縮起來很簡單。比如剛才那個例子,用 3 個 bit 表示一個字元。在解壓縮的時候,每次從文字中讀取 3 位二進位制,然後翻譯成對應的字元,但是,霍夫曼編碼是不等長的,每次應該讀取 1 位,還是 2 位還是 3 位等等來解壓縮呢,這個問題就導致霍夫曼編碼解壓縮起來比較複雜。為了避免解壓縮過程中的歧義,霍夫曼編碼要求各個字元的編碼之間不會出現某個編碼是另一個編碼字首的情況。

假設這 6 個字元出現的頻率從高到低依次是 a、b、c、d、e、f ,我們把它們分別編碼成下面這樣子,任何一個字元的編碼都不是另一個的字首,在解壓縮的時候,每次會讀取儘可能長的可解壓的二進位制串,所以在解壓縮的時候也不會有歧義。經過這種編碼壓縮之後,這 1000 個字元只需要 2100bits 就可以了。


儘管霍夫曼編碼的思想並不難理解,但是如何根據字元出現頻率的不同,給不同的字元進行不同長度的編碼呢?這裡的處理稍微有些技巧。

  • 我們把每個字元看作一個節點,並且附帶著把頻率放到優先順序佇列中。
  • 我們從佇列中取出頻率最小的兩個節點 f(20)、e(30)
  • 然後新建一個節點 x(50),把頻率設定為兩個節點的頻率之和,並把這個新節點 x(50)作為 f(20)和 e(30)的父節點。
  • 最後再把 x(50)放入到優先順序佇列中。重複這個過程,直到佇列中沒有資料。
  • 現在我們統一給每一條邊畫一個權值,指向左子節點的邊我們通通標記為 0,指向右子節點的邊,我們通通標記為1,那麼從根節點到葉節點的路徑就是葉節點對應字元的霍夫曼編碼。

10、棧和佇列轉換程式碼

用佇列實現棧

#include <iostream>
#include "LinkQueue.h"
#include "LinkStack.h"
using namespace std;
using namespace DTLib;
template < typename T >
class QueueToStack : public Stack<T>
{
	protected:
	    LinkQueue<T> m_queue_1;
	LinkQueue<T> m_queue_2;
	LinkQueue<T>* m_pIn;
	LinkQueue<T>* m_pOut;
	void move() const   //O(n)
	{
		int n = m_pIn->length() - 1;
		for (int i = 0; i < n; i++)
		        {
			m_pOut->add(m_pIn->front());
			m_pIn->remove();
		}
	}
	void swap() //O(1)
	{
		LinkQueue<T>* temp = NULL;
		temp = m_pIn;
		m_pIn = m_pOut;
		m_pOut = temp;
	}
	public:
	    QueueToStack()
	    {
		m_pIn = &m_queue_1;
		m_pOut = &m_queue_2;
	}
	void push(const T &e)   //O(1)
	{
		m_pIn->add(e);
	}
	void pop()      //O(n)
	{
		if(m_pIn->length() > 0)
		        {
			move();
			m_pIn->remove();
			swap();
		} else
		        {
			THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
		}
	}
	T top() const   //O(n)
	{
		if(m_pIn->length() > 0)
		        {
			move();
			return m_pIn->front();
		} else
		        {
			THROW_EXCEPTION(InvalidOperationException, "No element in current stack ...");
		}
	}
	void clear()    //O(n)
	{
		m_queue_1.clear();
		m_queue_2.clear();
	}
	int size() const
	    {
		return m_queue_1.length() + m_queue_2.length();
	}
}
;
int main()
{
	QueueToStack<int> qs;
	for (int i = 0; i < 10; i++)
	    {
		qs.push(i);
	}
	while(qs.size() > 0)
	    {
		cout << qs.top() << "  ";
		qs.pop();
	}
	return 0;
}

用棧實現佇列

#include <iostream>
#include "LinkQueue.h"
#include "LinkStack.h"
 
 
using namespace std;
using namespace DTLib;
 
template < typename T >
class StackToQueue : public Queue<T>
{
protected:
    mutable LinkStack<T> m_stack_in;
    mutable LinkStack<T> m_stack_out;
 
    void move() const   //O(n)
    {
        if(m_stack_out.size() == 0)
        {
            while(m_stack_in.size() > 0)
            {
                m_stack_out.push(m_stack_in.top());
                m_stack_in.pop();
            }
        }
    }
 
public:
    void add(const T &e)    //O(1)
    {
        m_stack_in.push(e);
    }
    void remove()   //O(n)
    {
        move();
 
        if(m_stack_out.size() > 0)
        {
            m_stack_out.pop();
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException, "No element in current queue ...");
        }
    }
    T front() const     //O(n)
    {
        move();
 
        if(m_stack_out.size() > 0)
        {
            return m_stack_out.top();
        }
        else
        {
            THROW_EXCEPTION(InvalidOperationException,"No element in current queue ...");
        }
    }
    void clear()    //O(n)
    {
        m_stack_in.clear();
        m_stack_out.clear();
    }
 
    int length() const  //O(1)
    {
        return m_stack_in.size() + m_stack_out.size();
    }
 
};
 
int main()
{
    StackToQueue<int> sq;
 
    for(int i = 0; i < 10; i++)
    {
        sq.add(i);
    }
 
    while(sq.length() > 0)
    {
        cout << sq.front() << "  ";
 
        sq.remove();
    }
 
    return 0;
} 

小結

篇幅所限,我這裡就只講這十個題吧,畢竟面試題實在是太多了,除了這些之外,我還整理了一些別的大廠面試題

最後:

我想,可能還有很多人在今年剛過去的金三銀四春招中保持著觀望的形勢,害怕自己的能力不夠,或者是安於現狀,覺得目前拿著幾千的月薪覺得能夠接受,那麼你就要注意了,這是非常危險的!

我們身為技術人員,最怕的就是安於現狀,一直在原地踏步,那麼你可能在30歲就會迎來自己的職業危機,因為你工作這麼久提升的只有自己的年齡,技術還是萬年不變!

我知道,對於一些學歷沒有優勢的人來說,外包是別無選擇,但是未來的路究竟要怎麼走,取決你的步子邁多開。每個人都有自己的選擇,如果你喜歡穩定,那按部就班適合你,但你有想法,不甘平庸,那就別讓外包埋沒了你。

如果你想在未來能夠自我突破,圓夢大廠,那或許以上這份學習資料,你需要閱讀閱讀,希望能夠對你的職業發展有所幫助。

最後,希望未來的我發展順利,早日拿下p7!同樣,也祝願你實現自己的人生理想,願我們都越來越好,共勉!

獲取方式: 只需你點贊+關注後,Java進階交流群:714827309 哦-!


獲取方式: 只需你點贊+關注後,Java進階交流群:714827309 進群拿資料哦-!