Q1:什麼是 AVL 樹?
AVL 樹 是平衡二叉查找樹,增加和刪除節點後通過樹形旋轉重新達到平衡。右旋是以某個節點爲中心,將它沉入當前右子節點的位置,而讓當前的左子節點作爲新樹的根節點,也稱爲順時針旋轉。同理左旋是以某個節點爲中心,將它沉入當前左子節點的位置,而讓當前的右子節點作爲新樹的根節點,也稱爲逆時針旋轉。
Q2:什麼是紅黑樹?
紅黑樹 是 1972 年發明的,稱爲對稱二叉 B 樹,1978 年正式命名紅黑樹。主要特徵是在每個節點上增加一個屬性表示節點顏色,可以紅色或黑色。紅黑樹和 AVL 樹 類似,都是在進行插入和刪除時通過旋轉保持自身平衡,從而獲得較高的查找性能。與 AVL 樹 相比,紅黑樹不追求所有遞歸子樹的高度差不超過 1,保證從根節點到葉尾的最長路徑不超過最短路徑的 2 倍,所以最差時間複雜度是 O(logn)。紅黑樹通過重新着色和左右旋轉,更加高效地完成了插入和刪除之後的自平衡調整。
紅黑樹在本質上還是二叉查找樹,它額外引入了 5 個約束條件:① 節點只能是紅色或黑色。② 根節點必須是黑色。③ 所有 NIL 節點都是黑色的。④ 一條路徑上不能出現相鄰的兩個紅色節點。⑤ 在任何遞歸子樹中,根節點到葉子節點的所有路徑上包含相同數目的黑色節點。
這五個約束條件保證了紅黑樹的新增、刪除、查找的最壞時間複雜度均爲 O(logn)。如果一個樹的左子節點或右子節點不存在,則均認定爲黑色。紅黑樹的任何旋轉在 3 次之內均可完成。
Q3:AVL 樹和紅黑樹的區別?
紅黑樹的平衡性不如 AVL 樹,它維持的只是一種大致的平衡,不嚴格保證左右子樹的高度差不超過 1。這導致節點數相同的情況下,紅黑樹的高度可能更高,也就是說平均查找次數會高於相同情況的 AVL 樹。
在插入時,紅黑樹和 AVL 樹都能在至多兩次旋轉內恢復平衡,在刪除時由於紅黑樹只追求大致平衡,因此紅黑樹至多三次旋轉可以恢復平衡,而 AVL 樹最多需要 O(logn) 次。AVL 樹在插入和刪除時,將向上回溯確定是否需要旋轉,這個回溯的時間成本最差爲 O(logn),而紅黑樹每次向上回溯的步長爲 2,回溯成本低。因此面對頻繁地插入與刪除紅黑樹更加合適。
Q4:B 樹和B+ 樹的區別?
B 樹中每個節點同時存儲 key 和 data,而 B+ 樹中只有葉子節點才存儲 data,非葉子節點只存儲 key。InnoDB 對 B+ 樹進行了優化,在每個葉子節點上增加了一個指向相鄰葉子節點的鏈表指針,形成了帶有順序指針的 B+ 樹,提高區間訪問的性能。
B+ 樹的優點在於:
① 由於 B+ 樹在非葉子節點上不含數據信息,因此在內存頁中能夠存放更多的 key,數據存放得更加緊密,具有更好的空間利用率,訪問葉子節點上關聯的數據也具有更好的緩存命中率。
② B+樹的葉子結點都是相連的,因此對整棵樹的遍歷只需要一次線性遍歷葉子節點即可。而 B 樹則需要進行每一層的遞歸遍歷,相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有 B+樹好。但是 B 樹也有優點,由於每個節點都包含 key 和 value,因此經常訪問的元素可能離根節點更近,訪問也更迅速。
Q5:排序有哪些分類?
排序可以分爲內部排序和外部排序,在內存中進行的稱爲內部排序,當數據量很大時無法全部拷貝到內存需要使用外存,稱爲外部排序。
內部排序包括比較排序和非比較排序,比較排序包括插入/選擇/交換/歸併排序,非比較排序包括計數/基數/桶排序。
插入排序包括直接插入/希爾排序,選擇排序包括直接選擇/堆排序,交換排序包括冒泡/快速排序。
Q6:直接插入排序的原理?
穩定,平均/最差時間複雜度 O(n²),元素基本有序時最好時間複雜度 O(n),空間複雜度 O(1)。
每一趟將一個待排序記錄按其關鍵字的大小插入到已排好序的一組記錄的適當位置上,直到所有待排序記錄全部插入爲止。
public void insertionSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int insertNum = nums[i];
int insertIndex;
for (insertIndex = i - 1; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex--) {
nums[insertIndex + 1] = nums[insertIndex];
}
nums[insertIndex + 1] = insertNum;
}
}
直接插入沒有利用到要插入的序列已有序的特點,插入第 i 個元素時可以通過二分查找找到插入位置 insertIndex,再把 i~insertIndex 之間的所有元素後移一位,把第 i 個元素放在插入位置上。
public void binaryInsertionSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int insertNum = nums[i];
int insertIndex = -1;
int start = 0;
int end = i - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (insertNum > nums[mid])
start = mid + 1;
else if (insertNum < nums[mid])
end = mid - 1;
else {
insertIndex = mid + 1;
break;
}
}
if (insertIndex == -1)
insertIndex = start;
if (i - insertIndex >= 0)
System.arraycopy(nums, insertIndex, nums, insertIndex + 1, i - insertIndex);
nums[insertIndex] = insertNum;
}
}
Q7:希爾排序的原理?
又稱縮小增量排序,是對直接插入排序的改進,不穩定,平均時間複雜度 O(n^1.3^),最差時間複雜度 O(n²),最好時間複雜度 O(n),空間複雜度 O(1)。
把記錄按下標的一定增量分組,對每組進行直接插入排序,每次排序後減小增量,當增量減至 1 時排序完畢。
public void shellSort(int[] nums) {
for (int d = nums.length / 2; d > 0 ; d /= 2) {
for (int i = d; i < nums.length; i++) {
int insertNum = nums[i];
int insertIndex;
for (insertIndex = i - d; insertIndex >= 0 && nums[insertIndex] > insertNum; insertIndex -= d) {
nums[insertIndex + d] = nums[insertIndex];
}
nums[insertIndex + d] = insertNum;
}
}
}
Q8:直接選擇排序的原理?
不穩定,時間複雜度 O(n²),空間複雜度 O(1)。
每次在未排序序列中找到最小元素,和未排序序列的第一個元素交換位置,再在剩餘未排序序列中重複該操作直到所有元素排序完畢。
public void selectSort(int[] nums) {
int minIndex;
for (int index = 0; index < nums.length - 1; index++){
minIndex = index;
for (int i = index + 1;i < nums.length; i++){
if(nums[i] < nums[minIndex])
minIndex = i;
}
if (index != minIndex){
swap(nums, index, minIndex);
}
}
}
Q9:堆排序的原理?
是對直接選擇排序的改進,不穩定,時間複雜度 O(nlogn),空間複雜度 O(1)。
將待排序記錄看作完全二叉樹,可以建立大根堆或小根堆,大根堆中每個節點的值都不小於它的子節點值,小根堆中每個節點的值都不大於它的子節點值。
以大根堆爲例,在建堆時首先將最後一個節點作爲當前節點,如果當前節點存在父節點且值大於父節點,就將當前節點和父節點交換。在移除時首先暫存根節點的值,然後用最後一個節點代替根節點並作爲當前節點,如果當前節點存在子節點且值小於子節點,就將其與值較大的子節點進行交換,調整完堆後返回暫存的值。
public void add(int[] nums, int i, int num){
nums[i] = num;
int curIndex = i;
while (curIndex > 0) {
int parentIndex = (curIndex - 1) / 2;
if (nums[parentIndex] < nums[curIndex])
swap(nums, parentIndex, curIndex);
else break;
curIndex = parentIndex;
}
}
public int remove(int[] nums, int size){
int result = nums[0];
nums[0] = nums[size - 1];
int curIndex = 0;
while (true) {
int leftIndex = curIndex * 2 + 1;
int rightIndex = curIndex * 2 + 2;
if (leftIndex >= size) break;
int maxIndex = leftIndex;
if (rightIndex < size && nums[maxIndex] < nums[rightIndex])
maxIndex = rightIndex;
if (nums[curIndex] < nums[maxIndex])
swap(nums, curIndex, maxIndex);
else break;
curIndex = maxIndex;
}
return result;
}
Q10:冒泡排序的原理?
穩定,平均/最壞時間複雜度 O(n²),元素基本有序時最好時間複雜度 O(n),空間複雜度 O(1)。
比較相鄰的元素,如果第一個比第二個大就進行交換,對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對,每一輪排序後末尾元素都是有序的,針對 n 個元素重複以上步驟 n -1 次排序完畢。
public void bubbleSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
for (int index = 0; index < nums.length - 1 - i; index++) {
if (nums[index] > nums[index + 1])
swap(nums, index, index + 1)
}
}
}
當序列已經有序時仍會進行不必要的比較,可以設置一個標誌記錄是否有元素交換,如果沒有直接結束比較。
public void betterBubbleSort(int[] nums) {
boolean swap;
for (int i = 0; i < nums.length - 1; i++) {
swap = true;
for (int index = 0; index < nums.length - 1 - i; index++) {
if (nums[index] > nums[index + 1]) {
swap(nums, index ,index + 1);
swap = false;
}
}
if (swap) break;
}
}
Q11:快速排序的原理?
是對冒泡排序的一種改進,不穩定,平均/最好時間複雜度 O(nlogn),元素基本有序時最壞時間複雜度 O(n²),空間複雜度 O(logn)。
首先選擇一個基準元素,通過一趟排序將要排序的數據分割成獨立的兩部分,一部分全部小於等於基準元素,一部分全部大於等於基準元素,再按此方法遞歸對這兩部分數據進行快速排序。
快速排序的一次劃分從兩頭交替搜索,直到 low 和 high 指針重合,一趟時間複雜度 O(n),整個算法的時間複雜度與劃分趟數有關。
最好情況是每次劃分選擇的中間數恰好將當前序列等分,經過 log(n) 趟劃分便可得到長度爲 1 的子表,這樣時間複雜度 O(nlogn)。
最壞情況是每次所選中間數是當前序列中的最大或最小元素,這使每次劃分所得子表其中一個爲空表 ,這樣長度爲 n 的數據表需要 n 趟劃分,整個排序時間複雜度 O(n²)。
public void quickSort(int[] nums, int start, int end) {
if (start < end) {
int pivotIndex = getPivotIndex(nums, start, end);
quickSort(nums, start, pivotIndex - 1);
quickSort(nums, pivotIndex + 1, end);
}
}
public int getPivotIndex(int[] nums, int start, int end) {
int pivot = nums[start];
int low = start;
int high = end;
while (low < high) {
while (low <= high && nums[low] <= pivot)
low++;
while (low <= high && nums[high] > pivot)
high--;
if (low < high)
swap(nums, low, high);
}
swap(nums, start, high);
return high;
}
Q12:歸併排序的原理?
歸併排序基於歸併操作,是一種穩定的排序算法,任何情況時間複雜度都爲 O(nlogn),空間複雜度爲 O(n)。
基本原理:應用分治法將待排序序列分成兩部分,然後對兩部分分別遞歸排序,最後進行合併,使用一個輔助空間並設定兩個指針分別指向兩個有序序列的起始元素,將指針對應的較小元素添加到輔助空間,重複該步驟到某一序列到達末尾,然後將另一序列剩餘元素合併到輔助空間末尾。
適用場景:數據量大且對穩定性有要求的情況。
int[] help;
public void mergeSort(int[] arr) {
int[] help = new int[arr.length];
sort(arr, 0, arr.length - 1);
}
public void sort(int[] arr, int start, int end) {
if (start == end) return;
int mid = start + (end - start) / 2;
sort(arr, start, mid);
sort(arr, mid + 1, end);
merge(arr, start, mid, end);
}
public void merge(int[] arr, int start, int mid, int end) {
if (end + 1 - start >= 0) System.arraycopy(arr, start, help, start, end + 1 - start);
int p = start;
int q = mid + 1;
int index = start;
while (p <= mid && q <= end) {
if (help[p] < help[q])
arr[index++] = help[p++];
else
arr[index++] = help[q++];
}
while (p <= mid) arr[index++] = help[p++];
while (q <= end) arr[index++] = help[q++];
}
Q13:排序算法怎麼選擇?
數據量規模較小,考慮直接插入或直接選擇。當元素分佈有序時直接插入將大大減少比較和移動記錄的次數,如果不要求穩定性,可以使用直接選擇,效率略高於直接插入。
數據量規模中等,選擇希爾排序。
數據量規模較大,考慮堆排序(元素分佈接近正序或逆序)、快速排序(元素分佈隨機)和歸併排序(穩定性)。
一般不使用冒泡。
本文分享自微信公衆號 - JAVA日知錄(javadaily)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。