算法基础面试题(一)
排序
选择排序
选择排序就是从数组中选择出来最大或者最小的元素,然后将其和队首或者队尾的元素进行交互。时间复杂度:O(n^2)
public class SelectionSort {
public static void selectionSort(int[] arr) {
int n = arr.length;
// 外层循环控制选择排序的轮数
for (int i = 0; i < n - 1; i++) {
// 假设当前轮次的第一个元素是最小的
int minIndex = i;
// 内层循环在未排序的部分中找到最小元素的索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将找到的最小元素与当前轮次的第一个元素交换位置
swap(arr, i, minIndex);
}
}
// 辅助方法用于交换数组中的两个元素
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
冒泡排序
冒泡排序的原理很简单,我们想象一下一个一个的气泡上浮的过程。排序共进行八轮,每一轮都会做两两比较,并将较大的元素右移,就像冒泡一下。
一轮结束之后,八个元素中最大的那个元素44将会移动到最右边。
然后再重复其他的几轮。最终得到一个完全排序的数组。
时间复杂度O(n^2)。
归并排序
归并排序简称Merge sort是一种递归思想的排序算法。这个算法的思路就是将要排序的数组分成很多小的部分,直到这些小的部分都是已排序的数组为止(只有一个元素的数组)。
然后将这些排序过的数组两两合并起来,组成一个更大一点的数组。接着将这些大一点的合并过的数组再继续合并,直到排序完整个数组为止。
时间复杂度为 O(nlogn)
public class MergeSort {
public static void mergeSort(int[] arr, int low, int high) {
if (low < high) {
int mid = (high -low) / 2+ low;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
private static void merge(int[] arr, int low, int mid, int high) {
int[] temp = new int[arr.length];
int i = low;
int j = mid + 1;
int k = low;
while (i <= mid && j <= high) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) { // 合并左边剩余元素
temp[k++] = arr[i++];
}
while (j <= high) { // 合并右边剩余元素
temp[k++] = arr[j++];
}
for (int l = low; l <= high; l++) { // 将合并后的结果拷贝回原数组
arr[l] = temp[l];
}
}
}
插入排序
插入排序就是将要排序的元素插入到已经排序的数组中,从而形成一个新的排好序的数组。时间复杂度为 O(n^2)
public class InsertionSort {
public static void insertionSort(int[] arr) {
int n = arr.length;
// 外层循环控制待插入的元素,从第二个元素开始
for (int i = 1; i < n; i++) {
// 当前要插入的元素
int key = arr[i];
// 内层循环用于在已排序的部分找到插入位置
int j = i - 1;
// 从后到前,移动比 key 大的元素向右,为 key 腾出插入位置
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 插入 key 到正确的位置
arr[j + 1] = key;
}
}
快速排序
快速排序也采用的是分而制之的思想。那么快速排序和归并排序的区别在什么地方呢?
归并排序是将所有的元素拆分成一个个排好序的数组,然后将这些数组再进行合并。
而快速排序虽然也是拆分,但是拆分之后的操作是从数组中选出一个中间节点,然后将数组分成两部分。
左边的部分小于中间节点,右边的部分大于中间节点。
然后再分别处理左边的数组合右边的数组。
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[low];
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
while (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
}
count排序
count排序是一种空间换时间的算法,我们借助一个外部的count数组来统计各个元素出现的次数,从而最终完成排序。
public class CountingSort {
public static void countingSort(int[] arr) {
int n = arr.length;
// 找到数组中的最大值,确定计数数组的大小
int max = findMax(arr);
// 初始化计数数组,大小为最大值加一
int[] count = new int[max + 1];
// 统计每个元素的出现次数
for (int value : arr) {
count[value]++;
}
// 根据计数数组重构原始数组
int index = 0;
for (int i = 0; i <= max; i++) {
while (count[i] > 0) {
arr[index++] = i;
count[i]--;
}
}
}
private static int findMax(int[] arr) {
int max = arr[0];
for (int value : arr) {
if (value > max) {
max = value;
}
}
return max;
}
- 计数排序的时间复杂度为 O(n + k),其中 n 是数组的长度,k 是数组中元素的范围(最大值减最小值加一)。
- 计数排序是一种稳定的排序算法,适用于一定范围内的整数排序,但对于范围较大的整数或浮点数排序并不适用。
- 计数排序是一种线性时间复杂度的排序算法,但需要额外的空间来存储计数数组。
堆排序
java中的PriorityQueue
public class HeapSort {
public static void heapSort(int[] arr) {
//不包含n
int n = arr.length;
// 构建最大堆,从最后一个非叶子节点开始向上调整
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 从堆顶依次取出最大元素,将堆重新调整为最大堆
for (int i = n - 1; i > 0; i--) {
// 将堆顶元素(最大值)与当前未排序部分的最后一个元素交换
swap(arr, 0, i);
// 调整剩余部分为最大堆,剩余部分不包含i
heapify(arr, i, 0);
}
}
//调整i这个节点,比较他跟他的子节点, 并递归继续调整被交换的larget节点
private static void heapify(int[] arr, int n, int i) {
int largest = i; // 初始化最大值的索引为当前节点
int leftChild = 2 * i + 1; // 左子节点的索引
int rightChild = 2 * i + 2; // 右子节点的索引
// 如果左子节点比当前节点大,则更新最大值的索引 ,为什么要小于n呢?是因为不包含n
if (leftChild < n && arr[leftChild] > arr[largest]) {
largest = leftChild;
}
// 如果右子节点比当前节点大,则更新最大值的索引
if (rightChild < n && arr[rightChild] > arr[largest]) {
largest = rightChild;
}
// 如果最大值的索引不等于当前节点,交换它们并递归调整子树
if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
技巧
二维前缀和?
单调队列?
计算n/t的结果,如果有余数则+1,可以简化为(n+t-1)/t 非常巧妙
检查整数的类型:正整数,负整数,符号
int * int 的值需要用long来存储。
链表求倒数可以用栈
求最大公约数,碾转相除法:
public int gcd(int a, int b) {
return b != 0 ? gcd(b, a % b) : a;
}
二进制运算
计算当前 x 和 y 的无进位相加结果:answer = x ^ y
计算当前 x 和 y 的进位:carry = (x & y) << 1
完成本次循环,更新 x = answer,y = carry
位运算: 抹去最右边的 1 : n = n & (n - 1);
保留最后一位:n&1
位运算 x & -x 取出 x 的二进制表示中最低位那个 1
~a 按位取反
i - (i >>> 1) 得到i的最高位的1
只出现一次的数组---异或,相同的数字异或结果是0,0和任何数字异或是数字本身
>>(带符号右移): 对于正数,>> 和 >>> 的效果是相同的。
>>>(无符号右移): 与 >> 不同,>>> 在移动过程中不管符号位,总是使用0来填充左侧的空位。
//数字范围按位与----选择数字范围的公共二进制前缀
public int rangeBitwiseAnd(int left, int right) {
while (left < right) {
// 抹去最右边的 1
right = right & (right - 1);
}
return right;
}
public int hammingWeight(int n) {
int cnt=0;
while(n!=0){
cnt += (n & 1);
n >>>= 1;
}
return cnt;
}