作者:禅与计算机程序设计艺术
1.背景介绍
数据结构及其应用
数据结构(Data Structure)在计算机编程中扮演着重要角色,它是指相互之间存在一种或多种关系的数据元素的集合、一些对这些元素进行操作的规则及方法。如线性表、树形结构、图形结构、队列、栈、散列表、集合、堆、排序算法等。不同的数据结构在性能、存储量、实现难度等方面也有不同的衡量标准,所以选择合适的数据结构对于提升程序的效率、降低程序的资源消耗具有重要意义。
实际上,数据结构不仅直接影响到程序的性能和效率,而且也是程序设计中不可缺少的一环。例如,选择了错误的数据结构可能会导致程序运行出错、运行缓慢或者达不到预期效果,因此在应用数据结构之前,应充分考虑数据的规模、应用场景、算法复杂度、并发读写情况等诸多因素。此外,熟练掌握各种数据结构,可以帮助我们更加深入地理解各种算法的工作原理,更好地运用它们解决实际的问题。
选取何种数据结构
一般情况下,如果已经有了一定的经验,那么可以通过分析已有的代码或者抽象的业务逻辑来决定采用哪种数据结构。但更多时候,只有在遇到特定问题的时候,才会去深究某个特定的数据结构的优劣势、适用场景及其实现。比如,当需要处理海量数据时,为了快速查找某个关键字,使用哈希表是一个好的选择;当需要频繁地插入、删除、修改数据项时,可以使用树形结构(如红黑树、B-树);当需要动态管理内存时,可以使用堆栈或双端队列;而当数据的规模比较小(比如几百个数据项)且需要随机访问时,则使用数组是一个不错的选择。总而言之,选择合适的数据结构,可以有效地提升程序的性能、节省内存空间、降低资源消耗。
另外,当遇到新需求时,也可以根据实际情况,结合相关的算法原理和时间复杂度进行权衡,然后选择一个最适合的基于该场景的最优数据结构。这样既能够尽可能地满足需求,又能保证程序的正确性和效率。
2.核心概念与联系
数据类型
数据类型通常是指数据所具有的特征,包括大小、表示形式、范围、单位、精度、可空值、编码方式等。常见的数据类型有整型、浮点型、字符型、布尔型、日期型、数组、指针、结构体、联合体等。
顺序表
顺序表是将同种类型的变量存放在一起,并排成一条线形结构的存储结构。顺序表中的各个数据项都按照先后顺序依次存放。顺序表通常可以进行两类基本操作:元素的插入和删除。
静态顺序表
静态顺序表是一个数组,即每一个位置上只存放一个元素。静态顺序表可以很方便地进行插入和删除操作,但由于分配和释放存储空间的开销较大,所以当数据项较多时,其查询速度就会受到限制。
动态顺序表
动态顺序表与静态顺序表类似,但它允许动态扩容和缩容,即当添加的元素超过当前的存储容量时,表自动再分配更多的存储空间。动态顺序表在某些情况下可以提供比静态顺序表更高的查询速度。
链表
链表是一种非连续的存储结构,它的每个节点除了保存数据项的值外,还有一个指向下一个节点的指针域。链表的插入、删除操作比较麻烦,但是提供了灵活的操作。链表的插入、删除可以在 O(1)的时间复杂度内完成,这对于某些要求高速插入删除的应用十分关键。
散列表
散列表(Hash Table)是一种键值对映射的数据结构,它通过计算索引值得到元素在存储区中的位置,从而极大的减少了搜索时间。散列函数可以把任意长度的二进制值转换为索引值。常用的散列函数有:除留余数法、乘积法、平方探测法、链地址法、开放寻址法等。
栈
栈是一种特殊的线性表,只能在顶端进行插入和删除操作。栈顶的元素被称作栈顶,栈底的元素被称作栈底。栈是先进后出的 Last In First Out (LIFO) 的数据结构。
队列
队列是一种特殊的线性表,只能在队尾进行插入操作,只能在队头进行删除操作。队首的元素被称作队头,队尾的元素被称作队尾。队列是先进先出的 First In First Out (FIFO) 的数据结构。
集合
集合(Set)是一种无序的集合,其中不包含重复元素。主要用于对一组元素进行一些基本操作,如判断某个元素是否属于集合、求两个集合之间的差集、并集等。
堆
堆(Heap)是一种特殊的树形数据结构,其根结点的值最大(最小),其他结点都要满足二叉树的定义,并且左子树的值都小于等于右子树的值。堆的应用很多,如调度算法、优先级队列等。
优先级队列
优先级队列是一种特殊的队列,用于处理具有优先级的数据。优先级队列按优先级对元素进行排序,每次删除队头元素都是优先级最高的元素。有两种常见的优先级队列:最小堆和最大堆。
3.核心算法原理和具体操作步骤以及数学模型公式详细讲解
排序算法
排序算法(Sort Algorithm)是指用来对记录集合进行重新排列的一种算法。常见的排序算法有:冒泡排序、插入排序、选择排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序等。
插入排序
插入排序(Insertion Sort)是一种简单的排序算法,它的核心思想是在要排序的一组数中,假设前面n-1个数已经排好序,现在要将第n个数插进去,使得这n个数依然排好序,算法如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
示例代码:
public void insertionSort(int[] arr){
int n = arr.length;
for(int i=1;i<n;i++){
int key = arr[i]; // 待插入元素
int j = i - 1; // 有序序列最后一个元素下标
while(j>=0 && arr[j]>key){
arr[j+1] = arr[j]; // 移动元素至后一个位置
j--; // 更新下标
}
arr[j+1] = key; // 插入新元素
}
}
平均时间复杂度:O(n^2)
冒泡排序
冒泡排序(Bubble Sort)是一种简单稳定的排序算法,它的核心思想就是重复地走访过要排序的数列,一次比较两个元素,交换它们的位置,直到没有任何一对数字需要比较,就说明已经排好序了。算法如下:
- 比较相邻的元素,如果第一个比第二个大,就交换他们两个
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
示例代码:
public void bubbleSort(int[] arr){
int n = arr.length;
for(int i=0;i<n-1;i++){
for(int j=0;j<n-i-1;j++){
if(arr[j]>arr[j+1]){
// swap arr[j] and arr[j+1]
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
平均时间复杂度:O(n^2)
选择排序
选择排序(Selection Sort)是一种简单直观的排序算法,它的核心思想是找到最小(大)的元素,放在第一位,然后从剩余的元素中继续找最小(大)的元素,放在第二位,以此类推,直到排序完成。算法如下:
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 重复第二步,直到所有元素均排序完毕
示例代码:
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[minIndex]>arr[j]){
minIndex = j;
}
}
// swap arr[i] and arr[minIndex]
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
平均时间复杂度:O(n^2)
希尔排序
希尔排序(Shell Sort)是插入排序的一种更高效的改进版本,其核心思想是将记录按下标的一定增量分组,对每组使用直接插入排序算法排序。随着增量逐渐减少,整个排序过程将变得更加有序,效率也会提高。算法如下:
- 设置一个gap序列,将gap序列的最后一个元素设置为数组的长度的一半
- 通过循环将各自gap的元素插入到对应位置
- gap减小,设置新的gap
- 当gap为1时,整个序列已排好序,退出循环
示例代码:
public void shellSort(int[] arr){
int n = arr.length;
// 计算间隔序列
int d = 1;
while((d/9)*7 < n/3){
d = (d*3 + 1)/2;
}
// 进行希尔排序
for(int g=d;g>0;g/=3){
for(int i=g;i<n;i+=g){
int temp = arr[i];
int j;
for(j=i;j>=g && arr[j-g]>temp;j-=g){
arr[j] = arr[j-g];
}
arr[j] = temp;
}
}
}
平均时间复杂度:O(n^(3/2))
归并排序
归并排序(Merge Sort)是创建在归并操作上的一种有效的排序算法,其性能稳定,是一种递归式的排序算法。归并排序首先递归地把当前序列拆分成两半,然后两边分别排序,再合并两个排序结果。算法如下:
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列调用归并排序
- 将两个排序后的子序列合并成一个最终的排序序列
示例代码:
public void mergeSort(int[] arr){
int n = arr.length;
if(n<=1){
return;
}
int mid = n / 2;
int[] leftArr = new int[mid];
int[] rightArr = new int[n - mid];
System.arraycopy(arr, 0, leftArr, 0, mid);
System.arraycopy(arr, mid, rightArr, 0, n - mid);
mergeSort(leftArr);
mergeSort(rightArr);
merge(arr, leftArr, rightArr);
}
private static void merge(int[] arr, int[] leftArr, int[] rightArr){
int i = 0;
int j = 0;
int k = 0;
while(i<leftArr.length&&j<rightArr.length){
if(leftArr[i]<rightArr[j]){
arr[k++] = leftArr[i++];
}else{
arr[k++] = rightArr[j++];
}
}
while(i<leftArr.length){
arr[k++] = leftArr[i++];
}
while(j<rightArr.length){
arr[k++] = rightArr[j++];
}
}
平均时间复杂度:O(nlogn)
快速排序
快速排序(Quick Sort)是对冒泡排序的一种改进,它采用分治法(Divide And Conquer)策略来将一个串行数组(或list)分为两个子序列,其中一部分元素的排序码比另一部分元素的排序码小。然后再按此法重新排序,直到整个序列有序为止。算法如下:
- 从数列中挑出一个元素,称为 “基准”(pivot)
- 重新排序数列,所有元素比基准值小的摆放在左边,所有元素比基准值大的摆在右边(相同的数可以到任一边)。在这个分割之后,基准元素的左边或者右边都是一个排好序的子数列
- 对每一边的子数列重复第二步,直到所有子数列都排好序
- 再对整体数列重复第一步,直到整个数列有序
示例代码:
public void quickSort(int[] arr, int start, int end){
if(start >= end){
return;
}
int pivotIdx = partition(arr, start, end);
quickSort(arr, start, pivotIdx-1);
quickSort(arr, pivotIdx+1, end);
}
// 返回arr[l...r]的第k大元素的索引
private static int partition(int[] arr, int l, int r){
int v = arr[l];
int idx = l;
for(int i=l+1;i<=r;i++){
if(arr[i] > v){
idx++;
swap(arr, idx, i);
}
}
swap(arr, idx, l);
return idx;
}
private static void swap(int[] arr, int a, int b){
int t = arr[a];
arr[a] = arr[b];
arr[b] = t;
}
平均时间复杂度:O(nlogn),最坏O(n^2)
堆排序
堆排序(Heap Sort)是指利用堆这种数据结构实现的排序算法,堆是一个近似完全二叉树的结构,树中每个节点都满足父节点的值都小于或等于子节点的值,而对于堆来说,这个特性使得堆可以用数组来实现,并可以进行动态维护。算法如下:
- 创建堆(Max Heap or Min Heap)
- 将堆顶元素(最大值或最小值)放到数组末尾,并调整堆
- 反复执行第2步,直到堆为空
- 堆排序完成
示例代码:
public void heapSort(int[] arr){
int len = arr.length;
buildMaxHeap(arr, len); // 创建最大堆
for(int i=len-1;i>0;i--){
swap(arr, 0, i); // 堆顶元素放到末尾
siftDown(arr, 0, i); // 堆调整
}
}
private static void buildMaxHeap(int[] arr, int len){
for(int i=(len-2)/2;i>=0;i--){
siftDown(arr, i, len);
}
}
private static void siftDown(int[] arr, int i, int len){
int child;
int tmp = arr[i];
for(child=2*i+1;child<len;child=2*child+1){
if(child!=len-1 && arr[child]<arr[child+1]){ // 若有右孩子且右孩子大于左孩子
child++;
}
if(tmp<arr[child]){
break;
}
arr[i] = arr[child];
i = child;
}
arr[i] = tmp;
}
平均时间复杂度:O(nlogn)
计数排序
计数排序(Counting Sort)是一种非比较排序算法,它的核心思想是统计数组中每个元素出现的次数,然后根据实际情况创建相应大小的数组,使得每个元素都可以在O(1)时间内赋值给输出数组。算法如下:
- 确定待排序的数组元素的范围,假设数组元素的最小值是m,最大值是M,则创建一个大小为M-m+1的数组C
- 初始化数组C的所有元素值为0
- 遍历数组A,将每个元素值作为索引,将其对应的C[x]的值加1
- 根据C[x]的值重建数组B
- 返回数组B
示例代码:
public void countingSort(int[] arr){
int maxVal = Integer.MIN_VALUE;
int minVal = Integer.MAX_VALUE;
for(int val : arr){
maxVal = Math.max(maxVal, val);
minVal = Math.min(minVal, val);
}
int countArraySize = maxVal - minVal + 1;
int[] countArray = new int[countArraySize];
for(int val : arr){
countArray[val - minVal]++;
}
int pos = 0;
for(int i=0;i<countArraySize;i++){
while(countArray[i]>0){
arr[pos++] = i + minVal;
countArray[i]--;
}
}
}
平均时间复杂度:O(n+k)
桶排序
桶排序(Bucket Sort)是计数排序的扩展算法,它利用了函数的映射关系,将要排序的数据分到几个不同的桶里,然后对每个桶里的数据分别进行排序。算法如下:
- 定义一个固定数量的 buckets,假设有 n 个 buckets,遍历原始数组 A,将元素 e 分入 bucket A[(e-min)/(max-min)*(n-1)] 中。
- 遍历 bucket i,对 bucket i 中的元素进行排序,可以使用 any sorting algorithm,比如 insert sort, select sort, bubble sort, etc..
- 合并所有的 buckets,得到完整的排序数组。
示例代码:
public void bucketSort(int[] arr){
int numBuckets = 10;
// Determine minimum and maximum values in the array
int minValue = Integer.MAX_VALUE;
int maxValue = Integer.MIN_VALUE;
for(int value : arr){
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
// Create list of empty buckets to hold elements
List<List<Integer>> bucketList = new ArrayList<>();
for(int i=0; i<numBuckets; i++) {
bucketList.add(new ArrayList<>());
}
// Distribute each element into its appropriate bucket based on its value range
double bucketSize = ((double)(maxValue - minValue + 1))/numBuckets;
for(int value : arr) {
int index = (int)Math.floor(((value - minValue)/bucketSize));
if(index == numBuckets) {
index--;
}
bucketList.get(index).add(value);
}
// Sort each non-empty bucket using an Insertion Sort
for(List<Integer> bucket : bucketList) {
Collections.sort(bucket);
}
// Merge all sorted buckets together into one sorted array
int index = 0;
for(List<Integer> bucket : bucketList) {
for(int value : bucket) {
arr[index++] = value;
}
}
}
平均时间复杂度:O(n+k),其中 k 是桶的数量。
基数排序
基数排序(Radix Sort)是一种非比较排序算法,它的核心思想是将整数按位数切割,然后按每个位数上的数字的顺序排序。算法如下:
- 获取数组中的最大元素,确定最大位数d
- 以d为循环索引,从最低位开始,对待排序数列进行稳定排序
- 每轮循环结束时,数列的最大位值发生变化,即最大位值的范围变小了
- 一共执行d轮循环
示例代码:
public void radixSort(int[] arr) {
int m = getMax(arr);
boolean negative = false;
if(m < 0) {
negative = true;
m *= -1;
}
for(int exp = 1; m/exp > 0; exp*=10) {
countingSort(arr, exp, digitAtPosition(arr, arr[0], exp), negative);
}
if(negative) {
reverse(arr, 0, arr.length-1);
}
}
private static int getMax(int[] arr) {
int max = arr[0];
for(int i = 1; i < arr.length; i++) {
if(arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static void countingSort(int[] arr, int exp, int divisor, boolean negative) {
int size = 10;
int[] count = new int[size];
// Count frequencies of digits at position 'exp'
for(int i = 0; i < arr.length; i++) {
int digit = getDigit(arr[i], exp, negative);
if(digit == 0) {
continue;
}
count[digit]++;
}
// Calculate prefix sums of counts
for(int i = 1; i < size; i++) {
count[i] += count[i-1];
}
// Place each element in its correct place in output array
int[] output = new int[arr.length];
for(int i = arr.length-1; i >= 0; i--) {
int digit = getDigit(arr[i], exp, negative);
if(digit!= 0) {
output[count[digit]-1] = arr[i];
count[digit]--;
}
}
// Copy sorted elements back into original array
System.arraycopy(output, 0, arr, 0, arr.length);
}
private static int getDigit(int number, int exp, boolean negative) {
int digit = (number/exp)%10;
if(!negative) {
return digit;
} else {
if(digit == 0) {
return 10;
} else {
return digit * (-1);
}
}
}
private static void reverse(int[] arr, int low, int high) {
while(low < high) {
int temp = arr[low];
arr[low] = arr[high];
arr[high] = temp;
low++;
high--;
}
}
平均时间复杂度:O(nk),其中 k 是数组中元素的最大位数。