Hello,大家好。
最近整理计算机基础笔记时,有一个很有意思的发现:很多初学者会在笔试或面试题里同时遇到“ASC码表”和“快速排序”这两个知识点。表面看,一个是字符编码,一个是排序算法,好像没什么交集;但实际做工程时,字符排序、字符串字典序、甚至不少数据比对问题,底层都离不开“字符在码表中的位置”,而快速排序又是理解这些排序过程最经典的算法之一。
这篇文章会围绕“ASC码表”与“快速排序”展开,先讲清楚字符码值到底是什么,再用 C 语言和 Java 分别写一套可以直接运行的快速排序实现,最后用一个“按 ASCII 码值给字符排序”的完整案例,把这两个知识点串起来。如果你正准备学算法、复习数据结构,或者曾经被快速排序的边界条件绕晕过,这篇文章应该能帮上忙。
1. 从“ASC码表”与快速排序说起
1.1 ASC码表是什么
先说明一个小的命名差异:网上搜“ASC码表”时,搜出来的大多是“ASCII 码表”。ASC 是 ASCII 在部分中文教材和资料里的缩写叫法,全称是 American Standard Code for Information Interchange,即美国信息交换标准代码。
ASCII 是一种基于拉丁字母的字符编码标准,它用二进制数来表示字符。为什么需要编码?因为计算机内部只认 0 和 1,字符要能存储、传输和显示,就必须先被转换成一串二进制数。比如字符A在 ASCII 码表中对应十进制 65,即二进制01000001;字符a对应十进制 97。当程序执行char c = 'A';时,变量c在内存里保存的本质上就是数字 65。
标准 ASCII 使用 7 位二进制数表示字符,范围从 0 到 127,一共 128 个字符。之后的扩展 ASCII 使用 8 位,范围扩展到 0 到 255。需要特别注意的是,0 到 127 这一部分在不同系统之间基本是统一的,而 128 到 255 这一部分在不同编码方案里可能表示不同字符。
1.2 为什么算法入门绕不开快速排序
快速排序是计算机科学家 Tony Hoare 在 1960 年左右提出的排序算法。它采用分治思想:把一个大数组拆成两个小数组,分别解决后再合并结果,甚至不需要额外合并,因为元素在分区过程中已经移动到了正确位置。
快速排序在平均情况下时间复杂度为 O(n log n),而且它是原地排序,不需要像归并排序那样申请大量额外空间。正因如此,很多编程语言的内置排序函数在设计时都考虑过快速排序或其改进版本。理解快速排序,不只是为了应付面试,更有助于理解底层排序机制,以及为什么某些输入会让快排变慢。
1.3 这篇文章适合谁读
如果你属于下面几类读者,阅读本文会比较顺畅:
- 刚开始学数据结构的同学,想把快速排序完整啃下来。
- 准备笔试面试、需要手写排序算法的开发者。
- 需要处理字符排序、但搞不清为什么字符排序结果和预期不一样的开发者。
- 想理解字符编码与程序排序关系的零基础学习者。
阅读本文后,你至少能掌握三件事:
- ASCII 码表的整体结构,以及如何在代码中查看任意字符的码值。
- 快速排序的原理、C 与 Java 实现、核心边界条件。
- 如何按 ASCII 码对字符数组进行快速排序,并理解字符排序的本质。
2. ASCII 编码基础与码表速查
2.1 ASCII 编码的基本设计
ASCII 码表通常用“十进制 + 十六进制 + 字符”的形式展示。比如数字字符0的 ASCII 码是 48(十六进制 0x30),大写字母A是 65(十六进制 0x41),小写字母a是 97(十六进制 0x61)。
这些数字不是乱定的,它们之间有非常清晰的规律:
- 数字
0到9的 ASCII 码是连续的,范围 48 到 57。 - 大写字母
A到Z的 ASCII 码是连续的,范围 65 到 90。 - 小写字母
a到z的 ASCII 码是连续的,范围 97 到 122。 - 同一个字母的大小写 ASCII 码相差 32,例如
A是 65,a是 97,差值正好 32。
所以在做字符处理时,经常能看到char - '0'这样的写法。例如'8' - '0'等价于56 - 48,结果是整数 8。大小写转换也可以利用char + 32或char - 32实现,不过真实工程中更推荐使用标准库函数,因为可读性更好,也更安全。
2.2 ASCII 码表整体结构
整个 ASCII 码表可以按字符类型划分成几个区间,先看最核心的部分。
| 区间(十进制) | 字符类型 | 说明 |
|---|---|---|
| 0 - 31 | 控制字符 | 不可打印,主要用于控制终端,例如换行、回车等 |
| 32 | 空格 | 可打印字符的最小值 |
| 33 - 47 | 标点与符号 | 如!、"、#、$、%、&、'等 |
| 48 - 57 | 数字0到9 | 数字字符是连续的区间 |
| 58 - 64 | 标点与符号 | 如:、;、<、=、>、?、@ |
| 65 - 90 | 大写字母A到Z | 大写字母连续区间 |
| 91 - 96 | 标点与符号 | 如[、\、]、^、_、` |
| 97 - 122 | 小写字母a到z | 小写字母连续区间 |
| 123 - 126 | 标点与符号 | 如{、|、}、~ |
| 127 | DEL 删除字符 | 控制字符 |
程序员日常用得最多的几个 ASCII 码值,可以单独保存一份速查表:
| 字符 | 十进制 | 十六进制 | 二进制 |
|---|---|---|---|
换行\n | 10 | 0x0A | 0000 1010 |
回车\r | 13 | 0x0D | 0000 1101 |
| 空格 | 32 | 0x20 | 0010 0000 |
'0' | 48 | 0x30 | 0011 0000 |
'A' | 65 | 0x41 | 0100 0001 |
'a' | 97 | 0x61 | 0110 0001 |
建议收藏这份表,后面做算法题时,遇到字符比较、判断数字、大小写转换,会非常方便。
2.3 代码中如何查看字符的 ASCII 码值
不要死记硬背,实际开发时可以直接通过代码把字符转成整数。
先看 C 语言的写法。C 语言中char本质上是小整型,可以直接用%d输出:
#include <stdio.h> int main() { char ch = 'A'; printf("%c -> %d (十六进制: 0x%X)\n", ch, ch, ch); return 0; }运行结果:
A -> 65 (十六进制: 0x41)再看 Java 的写法。Java 中char是无符号 16 位整数,把它转成int同样可以得到字符编码值:
public class AsciiDemo { public static void main(String[] args) { char ch = 'a'; System.out.println(ch + " -> " + (int) ch); } }运行结果:
a -> 97这里需要补充一点:Java 中char本质上是 UTF-16 编码单元。对于 ASCII 范围内的字符,其 UTF-16 码元值和 ASCII 码值是一致的,所以上面的输出仍然可以直接理解为“字符的 ASCII 码”。但如果字符是中文或 emoji,它对应的就不再属于 ASCII,而是 Unicode 码点,输出结果会明显不同。
2.4 ASCII 和 Unicode、UTF-8 的关系
很多初学者会把 ASCII、Unicode、UTF-8 混在一起。其实可以这样理解:
- ASCII 是字符集,同时也是一种编码方案,它只规定了 128 个字符与数字的对应关系。
- Unicode 是更大的字符集,试图给全世界所有字符一个统一编号。
- UTF-8 是 Unicode 的一种存储编码方式,属于变长编码。
但有一个关键点是:Unicode 的前 128 个码点,设计成了与 ASCII 完全一致。也就是说,英文字母、数字、常用英文标点在 UTF-8 编码后的第一个字节,与 ASCII 编码其实是兼容的。这也是为什么很多字符串函数在处理纯英文时,可以直接按字节比较,行为与 ASCII 码表一致。
中文字符在 Unicode 中位于更靠后的码点区域,所以它没有“ASCII 码”。如果硬要去查中文字的 ASCII 码,得到的中文在不同编码方案里可能完全不同。这一条在后面的字符排序部分会再次用到。
3. 快速排序核心原理拆解
3.1 快速排序的宏观思路
快速排序的整个流程可以概括成三步:
- 在待排序区间中挑选一个元素作为基准值。
- 把区间内其他元素分成两部分:比基准值小的放在左边,比基准值大的放在右边。这个过程称为分区。
- 对左右两个分区分别递归执行同样的操作。
比如有下面这样一组数据:
[50, 30, 80, 40, 10, 70, 20, 60]假设每次都选最后一个元素作为基准,那么第一次分区会选 60 作为基准。经过一轮比较和交换后,数组可能变成这样:
比 60 小的部分:[50, 30, 40, 10, 20] 基准 60 比 60 大的部分:[80, 70]随后继续递归排序左右两边。这个过程看起来很像把一个复杂问题不断拆分成更小的问题,也就是典型的分治思想。
3.2 分区过程手动推导
为了理解分区函数,我们看一轮完整的分区过程。基准选最后一个元素 60,用两个下标:
i表示“小于基准区”的末尾位置,初始为-1。j从区间左端向右扫描到基准前一个位置。
初始数组:
下标:0 1 2 3 4 5 6 7 数组:50 30 80 40 10 70 20 60 基准:60逐步处理:
j=0,50 < 60,说明 50 应该放到小于区,把i加 1 到 0,并交换arr[0]与arr[0],相当于不动。j=1,30 < 60,小于区扩展,位置不变。j=2,80 > 60,不处理。j=3,40 < 60,把arr[2]与arr[3]交换,此时数组为[50, 30, 40, 80, 10, 70, 20, 60]。j=4,10 < 60,把arr[3]与arr[4]交换,数组为[50, 30, 40, 10, 80, 70, 20, 60]。j=5,70 > 60,不处理。j=6,20 < 60,把arr[4]与arr[6]交换,数组为[50, 30, 40, 10, 20, 70, 80, 60]。
最后把基准 60 放到小于区和大于区中间,也就是把arr[5]与arr[7]交换:
[50, 30, 40, 10, 20, 60, 80, 70]此时返回基准下标5。可以看到,下标 5 左边的元素都小于等于 60,右边的元素都大于等于 60,但左右两侧内部仍然是乱序的。接下来只需要递归处理[0, 4]和[6, 7]这两个区间,整个数组最终就会有序。
3.3 递归终止条件与边界写法
快速排序最常见的 bug 来自区间边界。递归的终止条件是:区间里没有元素或者只有一个元素,也就是low >= high。
在基准选最后一个元素的写法中,循环扫描范围是j从low到high - 1,不能把基准自己也算进去。分区结束后,交换arr[i + 1]与arr[high],把基准放到真正属于它的位置,然后返回i + 1。
如果基准选第一个元素,扫描方向通常要反过来,交换逻辑也会有差别。很多同学把两种模板记混,写出来的分区就乱套了。建议刚开始只掌握一种最顺手的写法。
4. 快速排序完整实现:C 语言与 Java
4.1 C 语言快速排序完整代码
先看 C 语言版本。为了保证可读性,我给每个函数都加上注释。
// 文件路径:quick_sort.c #include <stdio.h