Q1:什么是 AVL 树?
Q2:什么是红黑树?
红黑树在本质上还是二叉查找树,它额外引入了 5 个约束条件:① 节点只能是红色或黑色。② 根节点必须是黑色。③ 所有 NIL 节点都是黑色的。④ 一条路径上不能出现相邻的两个红色节点。⑤ 在任何递归子树中,根节点到叶子节点的所有路径上包含相同数目的黑色节点。
这五个约束条件保证了红黑树的新增、删除、查找的最坏时间复杂度均为 O(logn)。如果一个树的左子节点或右子节点不存在,则均认定为黑色。红黑树的任何旋转在 3 次之内均可完成。
Q3:AVL 树和红黑树的区别?
在插入时,红黑树和 AVL 树都能在至多两次旋转内恢复平衡,在删除时由于红黑树只追求大致平衡,因此红黑树至多三次旋转可以恢复平衡,而 AVL 树最多需要 O(logn) 次。AVL 树在插入和删除时,将向上回溯确定是否需要旋转,这个回溯的时间成本最差为 O(logn),而红黑树每次向上回溯的步长为 2,回溯成本低。因此面对频繁地插入与删除红黑树更加合适。
Q4:B 树和B+ 树的区别?
B+ 树的优点在于:
① 由于 B+ 树在非叶子节点上不含数据信息,因此在内存页中能够存放更多的 key,数据存放得更加紧密,具有更好的空间利用率,访问叶子节点上关联的数据也具有更好的缓存命中率。
② B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子节点即可。而 B 树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有 B+树好。但是 B 树也有优点,由于每个节点都包含 key 和 value,因此经常访问的元素可能离根节点更近,访问也更迅速。
Q5:排序有哪些分类?
内部排序包括比较排序和非比较排序,比较排序包括插入/选择/交换/归并排序,非比较排序包括计数/基数/桶排序。
插入排序包括直接插入/希尔排序,选择排序包括直接选择/堆排序,交换排序包括冒泡/快速排序。
Q6:直接插入排序的原理?
每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。
1 2 3 4 5 6 7 8 9 10 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 个元素放在插入位置上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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:希尔排序的原理?
把记录按下标的一定增量分组,对每组进行直接插入排序,每次排序后减小增量,当增量减至 1 时排序完毕。
1 2 3 4 5 6 7 8 9 10 11 12 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:直接选择排序的原理?
每次在未排序序列中找到最小元素,和未排序序列的第一个元素交换位置,再在剩余未排序序列中重复该操作直到所有元素排序完毕。
1 2 3 4 5 6 7 8 9 10 11 12 13 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:堆排序的原理?
将待排序记录看作完全二叉树,可以建立大根堆或小根堆,大根堆中每个节点的值都不小于它的子节点值,小根堆中每个节点的值都不大于它的子节点值。
以大根堆为例,在建堆时首先将最后一个节点作为当前节点,如果当前节点存在父节点且值大于父节点,就将当前节点和父节点交换。在移除时首先暂存根节点的值,然后用最后一个节点代替根节点并作为当前节点,如果当前节点存在子节点且值小于子节点,就将其与值较大的子节点进行交换,调整完堆后返回暂存的值。
1 2 3 4 5 6 7 8 9 10 11 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;         } } 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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:冒泡排序的原理?
比较相邻的元素,如果第一个比第二个大就进行交换,对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,每一轮排序后末尾元素都是有序的,针对 n 个元素重复以上步骤 n -1 次排序完毕。
1 2 3 4 5 6 7 8 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)         }     } } 
当序列已经有序时仍会进行不必要的比较,可以设置一个标志记录是否有元素交换,如果没有直接结束比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 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:快速排序的原理?
首先选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全部小于等于基准元素,一部分全部大于等于基准元素,再按此方法递归对这两部分数据进行快速排序。
快速排序的一次划分从两头交替搜索,直到 low 和 high 指针重合,一趟时间复杂度 O(n),整个算法的时间复杂度与划分趟数有关。
最好情况是每次划分选择的中间数恰好将当前序列等分,经过 log(n) 趟划分便可得到长度为 1 的子表,这样时间复杂度 O(nlogn)。
最坏情况是每次所选中间数是当前序列中的最大或最小元素,这使每次划分所得子表其中一个为空表 ,这样长度为 n 的数据表需要 n 趟划分,整个排序时间复杂度 O(n²)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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:归并排序的原理?
基本原理:应用分治法将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并,使用一个辅助空间并设定两个指针分别指向两个有序序列的起始元素,将指针对应的较小元素添加到辅助空间,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。
适用场景:数据量大且对稳定性有要求的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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:排序算法怎么选择?
数据量规模中等,选择希尔排序。
数据量规模较大,考虑堆排序(元素分布接近正序或逆序)、快速排序(元素分布随机)和归并排序(稳定性)。
一般不使用冒泡。