排序
1. 选择排序
Selection Sort
定义最小值的游标,循环比较,跳着把比起小的值交换到左边
不稳定,时间复杂度最好最坏平均都是 O(n^2),空间复杂度 O(1)
基本不用
void myprint(int arr[], int len)
{
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
void myswap(int arr[], int i, int j)
{
int temp;
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
class Solution
{
public:
void selection_sort(int arr[], int len) {
for (int j = 0; j < len-1; j++) { // 不妨把轮数写大一点,测试的时候降低
int minPos = j;
for (int i = j+1; i < len; i++) { // i是index,最大值取len-1
minPos = arr[minPos] > arr[i] ? i : minPos;
}
myswap(arr, minPos, j);
cout << "第" << j+1 << "轮排序:" << endl;
myprint(arr, len);
}
}
};
int main()
{
Solution s;
int Arr[] = { 2,0,22,51,6,324,-7,6,5,543,23 };
int len = sizeof(Arr) / sizeof(Arr[0]);
cout << "排序前:" << endl;
myprint(Arr, len);
cout << endl;
s.selection_sort(Arr, len);
system("pause");
return 0;
}
C++注意事项:数组传入函数时,尽量传入它的length,在函数内部计算 sizeof(arr) 会出错
debug trick:边界条件若不好确定,等写完之后在确定
打印中间结果,减少功能模块来定位bug
2. 冒泡排序
Bubble Sort
从左到右,比较相邻两个数字,大的交换到右边,每轮循环找出一个最大值
相邻两个交换,不跳跃,稳定排序。最优时间复杂度 O(1),最坏 O(n^2),平均 O(n^2),空间复杂度 O(1)
class Solution
{
public:
void bubble_sort(int arr[], int len) {
cout << "排序前:" << endl;
myprint(arr, len);
cout << endl;
int count = 0;
for (int j = 0; j < len - 1; j++) {
if (arr[j] < arr[j + 1]) count += 1;
}
if (count == len - 1) {
cout << "第" << 0 << "轮:" << endl;
myprint(arr, len);
return;
} // 判断事前是否已经排好
for (int j = 0; j < len - 1; j++) {
for (int i = 0; i < len - 1 - j; i++) {
if (arr[i] > arr[i + 1]) myswap(arr, i, i + 1);
}
cout << "第" << j+1 << "轮:" << endl;
myprint(arr, len);
}
}
};
3. 插入排序
Insert Sort
从第二个数开始,向前比较若比其小,则交换
最优时间复杂度 O(n),最坏 O(n^2), 空间复杂度 O(1),稳定
对基本有序的数组最好用
class Solution
{
public:
void insert_sort(int arr[], int len) {
cout << "排序前:" << endl;
myprint(arr, len);
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j > 0 && arr[j] < arr[j-1]; j--) // 前向交换,所以j--
myswap(arr, j, j-1);
cout << "第" << i+1 << "轮:" << endl;
myprint(arr, len);
}
}
};
4. 希尔排序
Shell Sort
指定一个gap值,每次缩小两倍,按照gap值划分等区间,在各自区间对应次序位置上排序,直到 gap=1
考虑完两个gap之后,再追加一个gap
跳着拍,不稳定排序,平均时间复杂度 O(n^1.3),空间复杂度 O(1)
升级版的插入排序
class Solution
{
public:
void shell_sort(int arr[], int len) {
cout << "排序前:" << endl;
myprint(arr, len);
cout << endl;
for (int gap = len/2; gap > 0; gap /= 2) {
for (int i = gap; i < len; i++)
for (int j = i; j > gap - 1 && arr[j - gap] > arr[j]; j -= gap)
myswap(arr, j, j - gap);
cout << "gap = " << gap << endl;
myprint(arr, len);
}
}
};
5. 归并排序
Merge Sort
用到了递归的概念:引自 @九章算法,https://www.zhihu.com/question/31412436
迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,因此迭代是从前往后计算的。
递归则是一步一步往前递推,直到递归基础,寻找一条路径, 然后再由前向后计算。迭代是从前往后计算的,而递归则是先从后往前推,然后再由前往后计算,有“递”又有“归”。
通俗来讲:引自@lishichengyan
一个小朋友坐在第10排,他的作业本被小组长扔到了第1排,小朋友要拿回他的作业本,可以怎么办?
他可以拍拍第9排小朋友,说“帮我拿第1排的本子”,而第9排的小朋友可以拍拍第8排小朋友,说“帮我拿第1排的本子”…如此下去,消息终于传到了第1排小朋友那里,于是他把本子递给第2排,第2排又递给第3排…终于,本子到手啦!
排序时:
先假定两截数组已经有序,merge
- 定义左右中游标和临时等长数组
- 在两截中依次比较,把小的复制到临时数组
- 把遗留下来的直接贴到临时数组
在sort中实现递归
- 写出递归基础条件,只有一个元素时结束递
- 对输入的边界中间劈开,左右sort排序
- 调用merge
完整数组是“最后一个小朋友”,基础条件是“第一个小朋友”,基础条件排好序 “拿到作业”,把排好序的往后传,直到传到完整数组,排好序
JAVA、Python对对象的排序都是归并排序,为了稳定度
class Solution
{
public:
void merge(int arr[], int LeftPtr, int RightPtr, int RightBound) // 假定两边已经排好顺序
{
int i = LeftPtr; // 指在前半截数组第一个位置, 可取[0, mid]
int j = RightPtr; // 指在后半截数组第一个位置, 可取[mid+1, RightBound]
int mid = RightPtr - 1; // 都是下标index
int * temp;
temp = new int[RightBound - LeftPtr + 1]; // 创建临时等长数组
int k = 0; // 指在temp第一个位置
while (i <= mid && j <= RightBound) // 把小的复制到temp
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
// 这里的i, j, k不是循环内局部变量,可以存储变化
// 把遗留下来的元素全部粘贴下来
while (i <= mid) temp[k++] = arr[i++];
while (j <= RightBound) temp[k++] = arr[j++];
for (int m = 0; m < RightBound - LeftPtr + 1; m++) arr[LeftPtr+m] = temp[m];
delete temp;
temp = NULL;
}
void sort(int arr[], int LeftPtr, int RightBound)
{
// 只有一个元素不排序,也是递归的基础条件
if (LeftPtr == RightBound) return;
// 分成两半
int mid = (LeftPtr + RightBound) / 2;
// 左边排序
sort(arr, LeftPtr, mid);
// 右边排序
sort(arr, mid+1, RightBound);
merge(arr, LeftPtr, mid+1, RightBound);
}
};
int main()
{
Solution s;
int arr[] = { 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1 };
myprint(arr, 15);
s.sort(arr, 0, 14);
myprint(arr, 15);
system("pause");
return 0;
}
C++在函数体中用变量定义数组的长度(VS2017):
int num;
int * temp;
temp = new int[num];
"""…………"""
delete temp;
temp = NULL;
时间复杂度 O(nlogn),空间复杂度 O(n),稳定排序
6. 快速排序
Quick Sort
选出轴(基准)
- 定义初始轴的位置(最后)
- 从左边找比轴大的index,从右边找比轴小的index
- 把大的值和小的值交换
- 把轴和大的值交换
- 返回轴的位置
递归
- 写出基础条件
- 定义轴的位置
- 轴左边排序
- 轴右边排序
平均时间复杂度 O(nlogn),空间复杂度 O(logn),不稳
int partition(int arr[], int leftBound, int rightBound) {
int pivot = arr[rightBound]; // 找到基准值
int left = leftBound;
int right = rightBound - 1; // 基准值前一个作为右边界
while (left <= right) {
while (left <= right && arr[left] <= pivot) left++; // 从左边找比pivot大的index
while (left <= right && arr[right] > pivot) right--; // 从右边找比pivot小的index
if (left < right) myswap(arr, left, right);
}
myswap(arr, left, rightBound); // 再把轴和大的交换
return left; // 返回轴的位置
}
class Solution
{
public:
void quick_sort(int arr[], int leftBound, int rightBound) {
if (leftBound >= rightBound) return; // 一个元素不用排,基础条件
int mid = partition(arr, leftBound, rightBound); // 返回轴的位置
quick_sort(arr, leftBound, mid - 1);
quick_sort(arr, mid + 1, rightBound);
}
};
7. 计数排序
Counting Sort
是桶排序的一种变式,非比较排序,适用于数据量大,但是取值范围小的场景
比如:快速得知高考名次;数万名员工 年龄排序
- 构造一个用于存放待排序数组下标值个数的数组 temp,注意是个数!
- 构造一个累加数组存放各个桶最后一个元素在原数组的index值,实际上是index+1,因为temp统计的是各个桶里的个数
- 构造结果排序数组,从原数组的最后一位逆向读取,根据adding放置到合适位置
时间复杂度 O(n+k),空间复杂度O(n+k),由于adding的存在所以是稳定排序
class Solution {
public:
void counting_sort(int arr[], int len) {
int index_max = 10; // 待排序数组下标范围大小
int * temp;
temp = new int[index_max];
for (int i = 0; i < index_max; i++)
temp[i] = 0;
for (int i = 0; i < len; i++)
temp[arr[i]]++;
int * adding;
adding = new int[index_max];
adding[0] = temp[0];
for (int i = 1; i < index_max; i++) adding[i] = temp[i] + adding[i - 1];
// adding数组中的每一个元素都代表了对应到temp数组中相应index表示的数在待排序数组中的末位
int * new_arr;
new_arr = new int[len];
for (int i = len - 1; i >= 0; i--) {
new_arr[adding[ arr[i] ] - 1] = arr[i];
adding[arr[i]]--;
}
cout << "排序后: " << endl;
myprint(new_arr, len);
delete temp;
delete adding;
temp = NULL;
adding = NULL;
}
};
其中比较关键的操作是:
for (int i = len - 1; i >= 0; i--) {
new_arr[adding[ arr[i] ] - 1] = arr[i]; // adding[ arr[i] ] - 1 才是下标值
adding[arr[i]]--;
}
8. 基数排序
Radix Sort
非比较排序,桶排序的一种,多关键字排序 (个位、十位、百位)
返回数组中最大数的位数
int maxValue(int arr[], int len) {
int max = 0;
int maxvalue = 1;
for (int i = 0; i < len; i++)
max = max > arr[i] ? max : arr[i];
for (int j = 0; j < 10; j++) {
int div = pow(10, j);
if (max / div != 0)
maxvalue++;
}
return maxvalue;
}
分别按照个位大小,十位大小,百位大小……等排序
class Solution {
public:
void radix_sort(int arr[], int len, int maxvalue) {
int num = len;
int * count;
count = new int[num];
int number;
for (int j = 0; j < maxvalue; j++)
{
int * adding;
adding = new int[10];
int * result;
result = new int[num];
for (int i = 0; i < 10; i++)
// 要清空,不然个数的也会留到十位数上
count[i] = 0;
int div = pow(10, j); // 统计各个桶里有多少
for (int i = 0; i < len; i++) {
number = arr[i] / div % 10;
count[number]++;
}
adding[0] = count[0];
for (int i = 1; i < 10; i++)
adding[i] = adding[i - 1] + count[i];
for (int i = len - 1; i >= 0; i--) {
result[adding[(arr[i] / div) % 10] - 1] = arr[i];
adding[(arr[i]/div) % 10]--; // 排放一个,桶里就会少一个
}
for (int i = 0; i < len; i++) // 先按照一个位的排
arr[i] = result[i];
myprint(result, len);
delete result;
delete adding;
result = NULL;
adding = NULL;
}
}
};
9. 堆排序
Heap Sort
-
用到二叉树的思想,按照数组的下标排成二叉树
-
把无序堆排成大根堆,依照从右到左,从上到小,比较左右叶子和父节点的大小,把最大的置顶
-
把堆顶的数和最右下角的叶子交换,完成一次有序元素的抽取,并让数组长度减一
-
循环2、3步,直到数组的长度等于1
一、由无序堆变成大根堆
最后一个带有叶子的父节点在数组中的下标为:
l
a
s
t
F
a
t
h
e
r
i
n
d
e
x
=
c
e
i
l
(
[
s
t
a
r
t
+
e
n
d
]
/
2
)
−
1
lastFather_{index} = ceil([start + end]/2) - 1
lastFatherindex=ceil([start+end]/2)−1
start 是数组的起始下标(为0),end 是数组最后一个位置的下标
由上式也可以推断出,当数组长度为偶数时,
l
a
s
t
F
a
t
h
e
r
i
n
d
e
x
=
[
s
t
a
r
t
+
e
n
d
]
/
2
−
1
lastFather_{index} = [start + end]/2 - 1
lastFatherindex=[start+end]/2−1
长度为奇数时,
l
a
s
t
F
a
t
h
e
r
i
n
d
e
x
=
[
s
t
a
r
t
+
e
n
d
]
/
2
lastFather_{index} = [start + end]/2
lastFatherindex=[start+end]/2
而父节点拿到后,其左叶子的下标为 father_index * 2 + 1,右叶子为 father_index * 2 + 2
void maxHeap(int arr[], int end)
{
// 1 计算出堆最后一个父节点的下标
int lastFather = (0 + end) % 2 == 0 ? (0 + end) / 2 - 1 : (0 + end) / 2;
// 4 循环找出最大的放在堆顶
for (int father = lastFather; father >= 0; father--) // 它的下标减一的也都是父节点
{
// 2 根据父节点推算出左右孩子的下标
int left = father * 2 + 1;
int right = father * 2 + 2;
// 3 在保证右孩子不越界的情况下,使用右孩子和父节点比较;或者直接左孩子与父亲比较
if (right <= end && arr[right] > arr[father]) myswap(arr, right, father);
if (arr[left] > arr[father]) myswap(arr, left, father);
}
}
二、排序
由无序堆变成大根堆
交换堆顶和右下角叶子
数组长度减一
class Solution
{
public:
void heap_sort(int arr[], int len)
{
for (int end = len - 1; end > 0;end--) {
maxHeap(arr, end);
myswap(arr, 0, end);
}
myprint(arr, len);
}
};
时间复杂度 O(nlogn),空间复杂度 O(1),不稳定排序
9. 总结
时间复杂度、空间复杂度、稳定性
排序 | 平均时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|
选择 | selection | n^2 | 1 | 不稳 |
冒泡 | bubble | n^2 | 1 | 稳 |
插入 | insert | n^2 | 1 | 稳 |
堆 | heap | nlog_2n | 1 | 不稳 |
希尔 | shell | n^1.3 | 1 | 不稳 |
归并 | merge | nlog_2n | n | 稳 |
快速 | quick | nlog_2n | log_2n | 不稳 |
桶 | bucket | n+k | n+k | 稳 |
计数 | counting | n+k | n+k | 稳 |
基数 | radix | n*k | n+k | 稳 |
《打油诗》
选泡插
快归堆希桶计基
恩方恩老恩一三
对恩加k恩乘k
不稳稳稳不稳稳
不稳不稳稳稳稳
适用场景
(1)当数据规模较小时候,可以使用简单的直接插入排序或者直接选择排序
(2)当文件的初态已经基本有序,可以用直接插入排序和冒泡排序
(3)当数据规模较大时,可以考虑使用快速排序,要求排序时是稳定的,可以考虑用归并排序
(4)数据明显有几个关键字或者几个属性组成,用基数排序
(5)数据量大,但是取值范围小的场景,用计数排序