mobile wallpaper 1
mobile wallpaper 2
mobile wallpaper 3
mobile wallpaper 4
2247 字
6 分钟
排序与二分查找
2026-03-12

快速排序#

快速排序是一种利用分治的思想对集合进行排序的一种算法,选一个基准值 pivot,把数组切成两段:≤ pivot 的在左,≥ pivot 的在右,再递归排左右子区间

快排的分治分三步:

  1. 选取基准值 x(pivot)。 一般取区间中某个位置的值(示例代码取 arr[l])。
  2. 划分(partition)。 用两个指针 i、j 分别从两端向中间走:i 向右跳过所有小于 x 的元素,j 向左跳过所有大于 x 的元素;当 i 停在一个 ≥ x 的元素上、j 停在一个 ≤ x 的元素上时,交换二者。一趟走完后,左边全是 ≤ x 的元素,右边全是 ≥ x 的元素。
  3. 递归处理左右两个子区间,直到区间长度为 1 或 0(l >= r 时直接返回,因为一个元素天然有序)。

模板中的几个细节:

  • 指针初始化为 i = l - 1j = r + 1,并采用 do...while 先移动指针、再判断——保证两个指针每轮至少走一步,不会原地死循环。
  • 基准值取 arr[l] 时,递归划分必须用 j(即 [l, j][j+1, r]);若基准取 arr[r],则递归要用 i。基准取端点与划分边界要”错开”,否则在元素全相等之类的极端数组上会出现无限递归。
  • 划分结束时左右区间的大小一定都比原区间小,因此递归一定终止。

复杂度: 平均 O(n log n)(每层划分 O(n),共约 log n 层);最坏情况(每次基准都取到最大/最小值,例如对已有序数组取端点)退化为 O(n²)。快排是不稳定排序——相等的元素可能因交换而改变相对顺序。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException {
//此处可以使用BufferedReader速度比Scanner快
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int[] arr = new int[100];
for (int i = 0; i < n; i++) {
arr[i] = scanner.nextInt();
}
quickSort(arr,0,n-1);
for (int i = 0; i < n; i++) {
System.out.println(arr[i]);
}
}
public static void quickSort(int[] arr,int l,int r){
//判断数组长度,为1或者为0直接返回
if(l >= r){
return;
}
//选取基准值,基准值在选取时要注意处理边界,在选择j的时候边界不能选arr[r],同理在选择i时边界是不能选取arr[l]
int x = arr[l];
//因为是先移动指针再判断所以将左指针左移1位,右指针右移一位
int i = l - 1 ;
int j = r + 1;
//将数组分为两边,一边小于边界值x,一边大于边界值x
while (i < j){
do i++;while (arr[i] < x);
do j--;while (arr[j] > x);
//若i所指数大于边界值x并且j所指数小于边界值x则进行交换
if(i < j){
int temp;
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
//递归
quickSort(arr,l,j);
quickSort(arr,j+1,r);
}
}

归并排序#

归并排序的核心在于**“分而治之” (Divide and Conquer)**。它的逻辑非常稳健:与其面对一个乱七八糟的长数组,不如不断把它切半,直到每个部分只剩下一个数字(一个数字自然是有序的),然后再一层层合并回来。

具体分三步:

  1. 分(divide): 取中点 p = (l + r) >> 1,把区间 [l, r] 分成 [l, p][p+1, r] 两半;
  2. 治(conquer): 递归地对左右两半分别排序;
  3. 合(merge): 此时两个子区间都已经有序,用双指针 i、j 分别指向两半开头,每次取 arr[i]arr[j] 中较小的一个放进临时数组 temp,直到其中一半取完,再把另一半剩余元素全部接上,最后把 temp 写回原数组对应位置。

归并与快排都是分治,但归并的重头戏在”合并”(先递归、后处理),快排的重头戏在”划分”(先处理、后递归)

性质: 时间复杂度稳定为 O(n log n)(与输入数据的分布无关),代价是需要 O(n) 的临时数组空间;归并是稳定排序——合并时写成 arr[i] <= arr[j] 优先取左边,相等元素的相对顺序就保持不变。另外,只要在合并过程中统计”每取一个右边元素时,左边还剩多少元素”,就可以顺便求出逆序对的数量,这是归并排序的经典应用。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] arr = new int[100];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
mergeSort(arr,0,n-1);
for (int i = 0; i < n; i++) {
System.out.println(arr[i]);
}
}
public static void mergeSort(int[] arr ,int l ,int r){
if(l >= r){
return;
}
int p = (l+r) >> 1;
mergeSort(arr,l,p);
mergeSort(arr,p+1,r);
int k = 0;
int i = l;
int j = p+1;
int[] temp = new int[100];
while (i<=p && j<=r){
if(arr[i] <= arr[j]) temp[k++] = arr[i++];
else temp[k++] = arr[j++];
}
while (i<=p) temp[k++] = arr[i++];
while (j<=r) temp[k++] = arr[j++];
for (i = l,j = 0; i<=r ; i++,j++) {
arr[i] = temp[j];
}
}
}

冒泡排序#

就像水底的气泡一样,大的元素通过不断地相邻比较和交换,每一轮都会“浮”到数组的末尾。

具体过程:第 1 轮从头到尾把相邻元素两两比较,大的向后”冒”,结束时最大元素到达最后一个位置;第 2 轮对前 n-1 个元素重复,使第二大元素到达倒数第二个位置……共进行 n-1 轮后数组整体有序。每轮内层只需比较到 n - 1 - i,因为末尾 i 个元素已经就位,无需再比。

代码中的 swapped 标记是一种优化:如果某一轮从头到尾没有发生任何交换,说明数组已经有序,直接提前退出——最好情况(数组本来就接近有序)下只需一轮扫描 O(n)。

复杂度: 平均和最坏均为 O(n²)(比较次数约 n(n-1)/2);是稳定排序。整体效率偏低,适合小数据量或作为教学演示。

public void bubbleSort(int[] arr) {
int n = arr.length;
// 外层循环控制排序轮数
for (int i = 0; i < n - 1; i++) {
boolean swapped = false; // 优化:如果某一轮没有交换,说明已经有序
// 内层循环进行相邻比较
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if (!swapped) break;
}
}

选择排序#

每一轮都从“待排序”区域中直接选出一个最小(或最大)的元素,存放到序列的起始位置。

具体过程:第 1 轮在 [0, n-1] 中找出最小元素的下标,与位置 0 交换;第 2 轮在 [1, n-1] 中找最小元素,与位置 1 交换……进行 n-1 轮后,前面 n-1 个位置就位,最后一个位置自然也就位了。

与冒泡排序的区别:冒泡靠相邻交换”搬运”元素,每次比较都可能交换;选择排序每轮只记录最小值的下标,一轮结束时才交换一次,因此交换次数最多 n-1 次,远少于冒泡。

复杂度: 比较次数固定约 n(n-1)/2,时间复杂度 O(n²);交换可能把元素跳过中间位置,因此它是不稳定排序。与冒泡一样属于简单但效率较低的入门排序。

public 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;
}
}
// 将找到的最小值与当前边界位置交换
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}

二分#

整数二分#

二分的本质其实是不断缩小区间并且所找答案也包括在区间之内,当区间足够小的时候答案自然水落石出。

二分的使用前提与本质: 二分不只用于单调序列。更本质的条件是”两段性”——数组(或答案区间)可以被分成两段,前半段满足某种性质、后半段不满足(或反之),那么就可以用二分找出这两段的分界点。单调性是最常见的”两段性”情形,但二分能做的题不一定要求严格单调。

两个整数二分模板:

  • 模板一(找左边界,即第一个 ≥ x 的位置): mid = (l + r) >> 1(下取整);若 arr[mid] >= x,说明答案在 mid 左边(含 mid),执行 r = mid;否则执行 l = mid + 1。区间只剩两个元素时 mid 落在 l 上,l = mid + 1 保证区间一定缩小,不会死循环。
  • 模板二(找右边界,即最后一个 ≤ x 的位置): mid = (l + r + 1) >> 1(上取整);若 arr[mid] <= x,说明答案在 mid 右边(含 mid),执行 l = mid;否则执行 r = mid - 1。区间只剩两个元素时 mid 落在 r 上,r = mid - 1 保证区间一定缩小。

记忆口诀: 只要更新写成 l = mid,mid 就必须 +1(上取整);更新写成 r = mid,mid 就不加 1(下取整)。每次把区间砍掉约一半,所以循环是 O(log n)。

关于”无解”: 二分模板本身永远有解——总能找到两段的分界点,但分界点未必是题目想要的答案。例如查找一个不存在的数 x 时,模板一返回的是”第一个 ≥ x 的位置”,该位置的值可能不等于 x,所以退出循环后需要再检查一次 arr[l] == x 之类的条件,才能判断题目意义上的无解(对应代码里输出 -1 -1 的分支)。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
for (int i = 0; i < m; i++) {
int x = sc.nextInt();//需要进行查找的数
int l = 0 ;
int r = n -1 ;
//二分查找最开始出现的位置,边界为arr[mid] >= x
while (l < r){
int mid = (l + r) >> 1;
if(arr[mid] >= x) r = mid;
else l = mid + 1;
}
//判断该数是否存在,在查找完最开始的位置之后,如果所查的数不存在,那么有arr[l] != x
//此处无解指的是题目无解的一种情况,二分是一定有解的
if(arr[l] != x) System.out.println("-1 -1");
else{
System.out.println(l);
l = 0 ;
r = n -1 ;
//二分查找最后出现的位置
while (l < r){
int mid = (l + r + 1) >> 1;
if(arr[mid] <= x) l = mid;
else r = mid -1;
}
System.out.println(l);
}
}
}
}

浮点数二分#

浮点数二分与整数二分的思路完全相同,区别在于实数除法没有”整除边界”问题:mid = (l + r) / 2 直接除,更新时一律 l = midr = mid(不需要 ±1)。

精度控制: 浮点数不存在”l 与 r 相邻”的概念,所以循环条件改为 while (r - l > eps)——当区间长度小于一个足够小的精度阈值 eps 时结束,此时 l 与 r 的差距已经可以忽略,取 l 或 r 都可作为答案。经验做法是 eps 比题目要求的精度再多两位小数:例如要求保留 6 位小数,就取 eps = 1e-8,避免舍入误差影响最后一位。循环次数约为 O(log(区间长度 / eps)),非常快。

边界注意:sqrt(x) 时若 x < 1(如 x = 0.04),其平方根 0.2 大于 x,初始区间 [0, x] 就不正确,上界应取 max(1, x);若 x 可能为负数,则需要在二分之前单独特判。

求根号x的值

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
double x = sc.nextInt();
double l = 0;
double r = x;
while (r - l > 1e-8){
double mid = (l + r) / 2;
if(mid * mid >= x) r = mid;
else l = mid;
}
System.out.println(l);
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
//有序数组
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
for (int i = 0; i < m; i++) {
//需要进行查找的数
int x = sc.nextInt();
int l = 0 ;
int r = n -1 ;
//二分查找
while (l < r){
int mid = (l + r) >> 1;
if(arr[mid] >= x) r = mid;
else l = mid + 1;
}
System.out.println(arr[l]);
}
}
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

排序与二分查找
https://blogstella.xyz/posts/算法入门之排序与二分查找/
作者
Stella
发布于
2026-03-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录