C++数据结构和算法:排序算法
为了便于测试,先写一个生成随机数组的方法。
1 pair<int*,int> GenerateRandomArray(int maxSize, int maxValue, int minValue) 2 { 3 //随机数组长度 4 const int len = rand()%maxSize+1; 5 int* arr = new int[len]; 6 7 for (int i = 0; i < len; i++) 8 { 9 arr[i] = rand()%(maxValue-minValue+1)+minValue; 10 } 11 return make_pair(arr,len); 12 }
返回值(数组,数组长度)
//////////冒泡排序//////////
第一轮:从第0位开始,依次比较 i 和 i+1 位,把大的换到 i+1 位,一直比较到 i+1 = N
第二轮:从第0位开始,依次比较 i 和 i+1 位,把大的换到 i+1 位,一直比较到 i+1 = N-1
......
第N-1轮:从第0位开始,比较 0 和 1 位,大的换到第1位,比较结束。
1 void BubbleSort(int arr[], int N) 2 { 3 for (int i = N-1; i > 0; i--) //第一轮比较了N-1次,第二轮N-2次,...,一直到1次 4 { 5 for (int j = 0; j < i; j++) //注意最大值是j+1=i,所以要比较到j<i 6 { 7 if (arr[j] > arr[j + 1]) 8 swap(arr[j], arr[j + 1]); 9 } 10 } 11 }
//////////选择排序//////////
第一轮:从第0位开始,一直到最后一位,找到最小的数,放在第0位
第二轮:从第1位开始,一直到最后一位,找到最小的数,放在第1位
......
第N-1轮:从第N-2位开始,比较第N-2和第N-1位,也就是最后两位,找到最小的放在N-2位,排序结束。
1 void SelectionSort(int arr[], int N) 2 { 3 for (int i = 0; i < N - 1 ; i++) //只剩最后一位时不用比较,所以一共比较了N-1次 4 { 5 int minIdx = i; 6 for (int j = i + 1; j < N; j++) //已经把第i位设为当前最小值,直接从i+1开始比到最后一位 7 { 8 if (arr[j] < arr[minIdx]) 9 minIdx = j; 10 } 11 swap(arr[minIdx], arr[i]); 12 } 13 }
//////////插入排序//////////
第一轮:从第1位往前到第0位,逐次比较,把更小的换到左边,共比较1次。
第二轮:从第2位往前到第0位,逐次比较,把更小的换到左边,共比较2次。
......
第N-1轮:从第N-1位往前到第0位,逐次比较,把更小的换到左边,共比较N次。
===>外循环:i:1~N-1;
===>内循环:j:i~0;
最简单的可以类比扑克牌抽牌理牌,每抽起来一张牌都从右往左一一比较,然后插入到合适位置。
1 void InsertionSort(int arr[], int N) 2 { 3 for (int i = 1; i < N; i++) 4 { 5 for (int j = i - 1; j >= 0; j--) 6 { 7 if (arr[j] > arr[j + 1]) 8 swap(arr[j], arr[j + 1]); 9 } 10 } 11 }
时间复杂度:冒泡、选择、插入的时间复杂度都为O(N2)
下面介绍两种时间复杂度为O(NlogN)的算法:归并排序、快速排序(最坏情况是O(N2))
在此之前,先介绍递归
//////////递归//////////
Q. 在[L,R]范围内找最大值
1 int MaxValue(int arr[], int L, int R) 2 { 3 //[L,R] 只有1个数时,直接返回这个数 4 if (L == R) 5 return arr[L]; 6 int mid = L + ((R - L) >> 1); 7 int leftMax = MaxValue(arr, L, mid); 8 int rightMax = MaxValue(arr, mid + 1, R); 9 return max(leftMax, rightMax); 10 }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
小技巧:如何选取中间节点?
mid = ( L + R ) / 2 ??? × 当数字特别大时,L+R可能会溢出
mid = L + ( R - L ) / 2 √ 避免溢出
还可以效率更高:mid = L + (R - L) >> 1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tips. 不建议用递归,容易栈溢出
∵ 每当程序执行一个函数调用,就会增加一层栈帧,当函数返回才会减一层栈帧。递归调用达到结束点才会退栈清栈。
∴ 可以用动态规划代替递归 或 修改栈的大小
//////////归并排序//////////
1 void Merge(int arr[], int low, int mid, int high) 2 { 3 int i = low, j = mid + 1; 4 vector<int> tmpArr; 5 while (i <= mid && j <= high) 6 { 7 if (arr[i] < arr[j]) 8 tmpArr.push_back(arr[i++]); 9 else 10 tmpArr.push_back(arr[j++]); 11 } 12 while (i <= mid) 13 tmpArr.push_back(arr[i++]); 14 while (j <= high) 15 tmpArr.push_back(arr[j++]); 16 for (int n = 0; n <= high-low; n++) 17 arr[low+n] = tmpArr[n]; 18 } 19 20 //归并排序 21 void MergeSort(int arr[],int L, int R) 22 { 23 if (L >= R) return; 24 int mid = L + ((R - L) >> 1); 25 MergeSort(arr,L, mid); //左边排好序 26 MergeSort(arr,mid + 1, R); //右边排好序 27 Merge(arr, L, mid, R); //整体有序 28 }
Q. 荷兰国旗问题:给定一个数组arr,和一个数num,把小于num的数放在数组左边,等于num的数放在中间,大于num的数放右边,要求空间复杂度O(1),时间复杂度O(N)
思路:最左和最右两个指针i,j,指向当前位置的cur,从左往右依次遍历,如果比num大就跟j交换位置,j--;如果比num小就跟i交换位置,i++,cur++;如果等于num不用交换直接cur++。
1 void QuickSort1(int arr[], int len, int num) 2 { 3 int i = 0; //小于区域指针 4 int j = len-1; //大于区域指针 5 int cur = 0; //当前指针 6 7 while (cur < j) 8 { 9 if (arr[cur] < num) 10 { 11 swap(arr[cur], arr[i]); 12 i++; 13 cur++; 14 } 15 else if (arr[cur] == num) 16 cur++; 17 else 18 { 19 swap(arr[cur], arr[j]); 20 j--; 21 } 22 } 23 }
//////////快速排序//////////
1 void QuickSort(int arr[], int L, int R) 2 { 3 int i = L; 4 int j = R; 5 int temp = arr[i]; //基准值 6 if (i >= j) return; 7 while (i < j) 8 { 9 while (i < j && arr[j] >= temp) j--; //从右往左找到比基准值小的值 10 if (i < j) 11 arr[i++] = arr[j]; //将找到的比基准值小的数换到左边 12 while (i < j && arr[i] <= temp) i++; //从左往右找到比基准值大的数 13 if (i < j) 14 arr[j--] = arr[i]; //将找到的比基准值大的数换到右边 15 arr[i] = temp; //最后把基准数放在i位置 16 } 17 QuickSort(arr, L, i - 1); 18 QuickSort(arr, i + 1, R); 19 }
///大根堆///
堆逻辑上是一棵完全二叉树
0 i 的左孩子序号是:2i+1
1 2 右孩子序号是:2i+2
3 4 5 6 父节点序号是:(i-1)/2
每一棵子树的最大值是根节点,小根堆同理。
1 void HeapInsert(int arr[], int index) 2 { 3 while (arr[index] > arr[(index - 1) / 2]) //比父节点大则交换 4 { 5 swap(arr[index], arr[(index - 1) / 2]); 6 index = (index - 1) / 2; //继续向上检查父节点 7 } 8 } 9 int main() 10 { 11 for (int i = 0; i < len; i++) 12 HeapInsert(arr, i); 13 }
eg. 原始数组: 26 31 48 8 22
按大根堆插入之后:48 26 31 8 22
26 48
31 48 ==> 26 31
8 22 8 22
上面介绍了如何向上检查是否是大根堆,下面介绍如何向下检查:
1 void Heapify(int arr[], int index, int heapsize) 2 { 3 int left = index * 2 + 1; //左孩子的下标 4 while (left < heapsize) //如果左孩子下标没有越界,说明有左孩子 5 { 6 //比较左右孩子谁大赋给largest 7 int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ? left + 1 : left; 8 //比较大孩子跟父亲谁大赋给largest 9 largest = arr[largest] < arr[index] ? index : largest; 10 //如果父节点最大,不用交换,退出循环 11 if (largest == index) break; 12 //更大孩子与父节点交换位置,继续向下检查是否是大根堆 13 swap(arr[largest], arr[index]); 14 index = largest; 15 left = index * 2 + 1; 16 } 17 }
//////////堆排序//////////
1 void HeapSort(int arr[], int heapSize) 2 { 3 if (arr == nullptr || heapSize < 2) 4 return; 5 //向上检查大根堆,结果是最大的数排在根节点 6 for (int i = 0; i < heapSize; i++) //O(N) 7 HeapInsert(arr, i); //O(logN) 8 9 //把最大的数放到最后,heapSize理解为未排序的堆大小 10 swap(arr[0], arr[--heapSize]); 11 12 while (heapSize > 0) //O(N) 13 { 14 //向下检查大根堆,会把未排序最大值排到根节点 15 Heapify(arr, 0, heapSize); //O(logN) 16 //交换根节点与未排序末尾 17 swap(arr[0], arr[--heapSize]); 18 } 19 }
时间复杂度:O(NlogN)
过程演示:
7 28
27 28 ==HeapInsert==> 7 27
4 4
4
==heapSize=3,Swap==> 7 27
28
27
==Heapify==> 7 4
28
4
==heapSize=2,Swap==> 7 27
28
7
==Heapify==> 4 27
28
4
==heapSize=1,Swap==> 7 27
28
4
==Heapify==> 7 27
28
4
==heapSize=0,Swap==> 7 27
28
//////////基数排序//////////
原理:arr = {1,35,100,40,19}
数组中最大元素是3位(100),所以执行轮数是3轮
0~9 十个桶,每次按第i位数去装桶,再按先进先出原则输出顺序。
第一轮,比较 i=1 位
40
100 1 35 19
bucket:0 1 2 3 4 5 6 7 8 9
输出:100 40 1 35 19
第二轮:比较 i=2 位
1
100 19 35 40
bucket:0 1 2 3 4 5 6 7 8 9
输出:100 1 19 35 40
第三轮:比较 i=3 位
40
35
19
1 100
bucket:0 1 2 3 4 5 6 7 8 9
输出:1 19 35 40 100
找到最大位数:
1 int MaxBits(int arr[], int len) 2 { 3 int max = 0; 4 for (int i = 0; i < len; i++) 5 max = max > arr[i] ? max : arr[i]; 6 int res = 0; 7 while (max != 0) 8 { 9 res++; 10 max /= 10; 11 } 12 return res; 13 }
获取数字x的第d位数:
1 int GetDigit(int x, int d) 2 { 3 return ((x / (int)pow(10, d - 1)) % 10); 4 }
长度为1不用排直接返回,否则进行基数排序:
1 void RadixSort(int arr[],int len) 2 { 3 if (arr == nullptr || len < 2) 4 return; 5 RadixSort(arr, 0, len, MaxBits(arr, len)); 6 }
基数排序:每轮排序用到了键索引计数法(参考 上一篇字符串相关算法)
1 void RadixSort(int arr[], int L, int R, int digit) 2 { 3 int i = 0, j = 0; 4 vector<int> bucket; 5 for (int i = 0; i < R; i++) 6 bucket.push_back(0); 7 8 //根据最大位数确定轮数 9 for (int d = 1; d <= digit; d++) 10 { 11 int count[10] = { 0 }; //用于计算出现频率,count[i]代表d位≤i的数有多少个 12 for (int i = L; i <= R; i++) 13 { 14 j = GetDigit(arr[i], d); 15 count[j]++; 16 } 17 for (int i = 1; i < 10; i++) 18 count[i] += count[i - 1]; 19 20 for (i = R-1; i >= L; i--) 21 { 22 j = GetDigit(arr[i], d); 23 bucket[count[j] - 1] = arr[i]; 24 count[j]--; 25 } 26 for (int i = L, j = 0; i < R; i++, j++) 27 arr[i] = bucket[j]; 28 } 29 }
Tips. 上面这段代码函数返回时报count数组附近栈去损坏,还没找到原因
排序稳定性:不因为排序而改变相对次序就是稳定,比如 1 2 1 3,排序后 1 1 2 3,排序后的第一个1在原来的数组里也是第1个1,两个1的相对位置不变。
稳定的排序:冒泡、插入、归并、桶排序
不稳定的排序:选择、快排、堆排序
那么这个属性有什么用呢?比如多条件排序,先用了按时间排序,对于稳定的排序,再按下按价格排序,就会把最近最便宜的排在最上面,但如果用了不稳定的排序,那么添加第二个条件的时候最便宜的在最上面,但不一定是时间最近的了。