排序算法有很多中,我在这里只能粗略地介绍其中的6种,有什么不当之处还望读者多多指教。
为了方便起见,在这里假设排序为升序排列,用C++实现。
选择排序
选择排序是较为简单的一种排序方式,特点是在当前所指的元素前找比当前元素小的元素,如果找到就交换位置,是一种不稳定的排序方式,平均时间复杂度为O(n^2)。
具体实现:
for (int i = 1; i < n; i++) {//设定list[i]为被指向的数
for (int j = 0; j < i; j++) {//查找到被指向的元素为止
if (list[i] < list[j])//若后面的数比前面小,则交换位置
Swap(list[i], list[j]);
}
}
冒泡排序
冒泡排序也是一种较为简单的排序方式,特点是用每次用相邻两个元素比较,大的放在后面,然后右移指针。每一次可以选出一个最大的放在最后。这是一种稳定的排序方式,平均时间复杂度为O(n^2)。
优化思路:如果检测到序列已经排好序了,就不需要再次进行循环了,而是可以直接跳出。可以通过增加一个bool值来描述是否已经排好序。
但是,就算已经排好序,也需要经过一轮检测来得知是排好序的,所以无论如何都会经过一次遍历。
具体实现:
bool sorted = true;//先假设是排好序的
for (int i = n-1; i >=0 ; i--) {//要排序的元素数递减
for (int j = 0; j < i; j++) {
if (list[j] > list[j + 1]) {
//当一趟里前面的元素比后面的元素大时,交换
Swap(list[j], list[j + 1]);
sorted = false;//这时认为没有排好序
}
}
if (sorted) break;//如果已经排好序,跳出
}
插入排序
插入排序的特点是每次取一个元素插入之前已经排好序的序列中,如果前面的元素比较大,就后移一位,直到找到比要插入的元素小的元素或找到数组0号元素为止。第一次插入时将第一个元素自身视为有序序列。这是一种稳定的排序方式,平均时间复杂度为O(n^2)。
具体实现:
for (int i = 1; i < n; i++) {
int temp = a[i];//将当前元素存到临时元素中
int ti = i;//将当前位置也保存到临时位置
while (ti > 0 && a[ti - 1] > temp)
a[ti] = a[ti-- - 1];//当数组中元素比要插入的元素大时,后移一位
a[ti] = temp;//在满足条件的位置插入临时元素
}
基数排序
基数排序的特点是设置一个基数(假设为10),然后创建10个桶,每个桶最多有n(元素总个数)个元素,然后通过取模来按照余数的大小进行排序,先对个位排序,并把排序后的元素放回原数组中,再对十位排序……直到所有位都排序结束后,原数组就变为了有序数组。
这种排序方式是稳定的,平均时间复杂度是O(d(r+n)),其中r代表基数,d代表长度(共有几位),n代表元素个数。
具体实现:
T* bins[10];//10个桶
int p[10] = { 0 };//每个桶中放入了几个数
bool next = true;//是否要进行下一轮
int digit = 1;//每轮的位数
int remainder = 0;//每次的余数
for (int i = 0; i < 10; i++) {//初始化
bins[i] = new T[n];
}
do {
for (int i = 0; i < n; i++) {//把元素放入桶中
remainder = list[i] % (digit*10) / digit;
//当还有任意元素有余数时,循环要继续
if (remainder) next = true;
else next = false;
bins[remainder][p[remainder]++] = list[i];
//将元素放到余数桶的最后一个数的后面
}
int k = 0;//放置元素的位置
for (int i = 0; i < 10; i++) {
for (int j = 0; j < p[i]; j++) {
//把每个桶中的元素重新装到list中
list[k++] = bins[i][j];
}
}
for (int i = 0; i < 10; i++) //重置桶中放入的元素数
p[i] = 0;
digit *= 10;
} while (next);
for (int i = 0; i < 10; i++) {//释放空间
delete [] bins[i];
}
- 归并排序
归并排序是一种较为复杂的排序方法,特点是分而治之,将原数组先分成一个个小数组,再对每个小数组进行归并,形成一个排好序的大数组,再下一步归并,直到原数组整个被排好序为止。这是一种稳定的排序方式,时间复杂度为O(nlogn),需要一个存放临时数据的数组。我以前写过递归实现的伪代码,这里就再写一个非递归实现的代码吧。
具体实现:
template<class T>
void Merge(T* c, T* d, int l, int m, int r)
{
int i = l, //第一个块的指针
j = m + 1, //第二个块的指针
k = l; //结果的指针
//把小的先放进去,直到有一个块放完
while ((i <= m) && (j <= r)) {
if (c[i] < c[j]) d[k++] = c[i++];
else d[k++] = c[j++];
}
//把还没有放完的数据放进d数组
if (i > m)
for (int q = j; q <= r; q++)
d[k++] = c[q];
else
for (int q = i; q <= m; q++)
d[k++] = c[q];
}
template<class T>
void MergePass(T* a, T* b, int s, int n)
{
int i = 0;//从头开始进行
while (i <= n - 2 * s) {//能够分出长度为s的两段时,直接对两段归并
Merge(a, b, i, i + s - 1, i + 2 * s - 1);
i = i + 2 * s; //在数组中后移两个段的距离
}
//不能分成恰好的两段时,有两种情况:
//如果能分成两段,则分成一段长度为s的,一段包含剩下元素的,进行归并;
//不能分成两段,则直接拷贝到b数组中
if (i + s < n)
Merge(a, b, i, i + s - 1, n - 1);
else
for (int j = i; j <= n - 1; j++)
b[j] = a[j];
}
template<class T>
void MergeSort(T * a, int n)
{
T* b = new T[n]; //存放过程中数据的数组
int s = 1; //段长度
while (s < n) {
MergePass(a, b, s, n);//从a到b归并
s += s; //长度加倍
MergePass(b, a, s, n);//从b到a归并
s += s;
}
delete[] b;
}
快速排序
快速排序是一种较复杂的排序方式,但是性能十分优秀。它的特点是取一个数作为参照数,从左找比该数大的数,找到后再从右找比该数小的数,如果找到了这样一对数,就将它们交换位置,直到左边指针大于等于右边指针,这时把参照数和右指针指着的数交换位置,然后对该位置左边的数据和右边的数据分别进行归并排序,直到进入方法时的左指针就大于等于右指针时,直接返回。这是一种不稳定的排序方式,最坏时间复杂度为O(n^2),在数组本身就有序时出现,平均时间复杂度为O(nlogn)。
具体实现:
template<class T>
void quickSort(T* a, int left, int right,int n)
{
//当左指针大于等于右指针时,不需要排序,直接返回
if (left >= right) return;
int i = left, //从左到右的指针
j = right + 1; //从右到左的指针
T pivot = a[left]; //参照数
while (true) {//从左找大于参照数的数,从右找小于参照数的数
do {
i++;
} while (a[i] < pivot);//小于参照数,继续查找下一个
do {
j--;
} while (a[j] > pivot);//大于参照数,继续查找下一个
if (i >= j) break; //找不到要交换的对
Swap(a[i], a[j]);
}
//参照数与右指针所指的数交换位置
a[left] = a[j];
a[j] = pivot;
quickSort(a, left, j - 1,n);//对左半边排序
quickSort(a, j + 1, right,n);//对右半边排序
}
这样6个排序算法就大致讲完了,如果有什么错误与不足还望指正。