一、排序分类
排序的稳定性:关键字相同的元素经过排序后,相对顺序是否会改变。
1.插入排序
省流版:找出最值并往前放。
(1)代码
/*1.不带哨兵的插入排序*/
void InsertSort(int A[], int n)
{
int i, j, temp;
for (i = 1; i < n; i++)
{
if (A[i] < A[i - 1])//现在看下第i位是否需要放,若比最大的还小,则需要插入
temp = A[i]; //待放值先放到一个temp里
for (j = i - 1; j >= 0 && A[j] > temp; --j)//i之前的数,若比我大,都往后稍稍
A[j + 1] = A[j];//第j位把后面的位置占了,直到第二项不再满足,j+1项还空着
A[j + 1] = temp;//temp占j+1位。
}
}
/*2.把自己当哨兵的插入排序*/
void Insertsort(int A[], int n)
{
int i, j;
for (i = 2; i <= n; i++)//第0位是待覆盖的哨兵位,第1位肯定不需要排序,从第2开始排
A[0] = A[i];//覆盖哨兵位
for (j = i - 1; A[j] > A[0]; j--)
A[j + 1] = A[j];
A[j + 1] = A[0];
}
注意:插入排序带有稳定性。带哨兵的方式优点:不需要每轮都判断j是否大于等于0【防止越界】
不带哨兵,i从1开始,带哨兵的话i从2开始
2.折半插入排序:
省流版:找出最值并往前放。
void InsertSort(int A[],int n)/*不带哨兵*/
{
int temp;
int low, mid, high;
for (int i = 1; i < n; i++)
{
low = 0;
high = i-1;
temp = A[i];
while (high >= low)
{
mid = (low + high) / 2;
if (A[mid] > temp)
high = mid - 1;
else
low = mid + 1;
}
for (int j = i-1; j > high; j--)
//从有序的里面排序,所以从i-1开始,所以不是n
A[j + 1] = A[j];
A[low] = temp;
}
}
int main()
{
int i;
int a[8] = { 8,4,6,2,9,3,7,3 };
cout << "Before:"<<endl;
for (i = 0; i < 8; i++)
cout << a[i] << endl;
InsertSort(a, 8);
cout << "After:"<<endl;
for (i = 0; i < 8; i++)
cout << a[i] << endl;
return 0;
}
折半插入排序其实是在直接插入排序的基础上,结合了二分查找法的思想,顺序的二分查找替代了直接插入排序中遍历查找的过程,从而更快的能够确定待插入元素的位置,但是由于移动次数并没有发生改变,所以两者的时间复杂度相同(均为最小n、最大n方)。折半插入排序是稳定的,其时间复杂度为
3.希尔排序
(1)原理:利用增量预处理,实现直接插入前:数列是基本有序的。
(2)代码:
void ShellSort(int a[], int n)
{
for (int d = n / 2; d >= 1; d /= 2)
for (int i = 1+d; i <= n; i++)
if (a[i - d] > a[i])
{
a[0] = a[i];
int j;
for (j = i - d; j > 0 && a[j] > a[0]; j -= d)
a[j + d] = a[j];
a[j + d] = a[0];
}
}
时间复杂度为O(n^1.3)
希尔排序是不稳定的排序算法(因为跳着排序,不是依次那种)
二.交换排序
1.冒泡排序:
(1)意义
单向冒泡:每趟可以确定前一位最小数字
双向冒泡:
奇数趟时,从前往后比较,遇到逆序就交换,直到将最大元素移动到尾部。
偶数趟时,从后往前比较,遇到逆序就交换,直到将最小元素移动到首位。
(2)代码
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void BubbleSort(int A[], int n)
{
for (int i = 0; i < n - 1; i++)//每一趟会确定前面一位数字
{
bool flag = false;//表示本趟是否发生过交换
for(int j=n-1;j>i;j--)
if (A[j - 1] > A[j])
{
swap(A[j - 1], A[j]);
flag = true;
}
if (flag == false)
return;
}
}
void D_BubbleSort(int A[], int n)//双向起泡排序,交替两个方向起泡
{
int low = 0, high = n - 1;
bool flag = true;//是否发生交换的标志,若没有,则可以退出循环
while (low < high && flag)
{
flag = false;//每次置flag为false,进入循环
for (int i = low; i < high; i++)//找一个最大的放右边
if (A[i] > A[i + 1])
{
swap(A[i], A[i + 1]);
flag = true;//发生交换,下次冒泡还需要进行。
}
high--;
for (int i =high; i >low; i--)
if (A[i] < A[i - 1])
{
swap(A[i], A[i - 1]);
flag = true;//发生交换,下次冒泡还需要进行。
}
low++;
}
}
(3)性能分析
空间复杂度:O(1)
时间复杂度:平均为O(n^2)
最好情况=比较n-1 交换0 所以最好情况:O(n)
最坏情况=比较n(n-1)/2 交换是比较的3倍。 所以最坏情况O(n^2)
冒泡排序是稳定的排序。
2.快速排序
(1)分层
(2)代码
int Partition(int A[], int low, int high)
{
int pivot = A[low];
while (low < high)
{
while (low < high && A[high] >= pivot) --high;//如果high所指的更大,则让high一直左移,直到不满足上述条件
A[low] = A[high];//出现high小的,换到pivot位置上
while (low < high && A[low] <= pivot) ++low;//LOW右移
A[high] = A[low];
}
A[low] = pivot;
return low;
}
void QuickSort(int A[], int low, int high)
{
if (low < high)
{
int pivotpos = Partition(A, low, high);
QuickSort(A, low, pivotpos - 1);//递归算法,也可以用栈代替
QuickSort(A, pivotpos+1,high);
}
}
(3)算法效率
快排算法优化思路: 尽量选择可以把数据中分的枢轴元素。eg:选头、中、尾三个位置的元素,取中间值作为枢轴元素/随机选一个元素作为枢轴元素。
(4)稳定性:快排不稳定
三、选择排序
1.简单选择排序
(1)代码
void SelectSort(int A[], int n)
{
for (int i; i < n - 1; i++)
{
int min = i;
for (int j = i + 1; j < n; j++)
if (A[j] < A[min]) min = j;
if (min != i) swap(A[i], A[min]);
}
}
(2)效率及稳定性
2.堆排序
(1)代码
/*1.大根堆第k个元素下坠过程*/
void HeadAdjust(int A[], int k, int len)
{
A[0] = A[k];
for (int i = 2 * k; i <= len; i *= 2)
{
if (i < len && A[i] < A[i + 1])
i++;
if (A[0] >= A[i]) break;
else
{
A[k] = A[i];
k = i;
}
}
}
/*2.建立大根堆*/
void BuildMaxHeap(int A[], int len){
for (int i = len / 2; i > 0; i--)
HeadAdjust(A, i, len);
}
/*3.利用已有的大顶堆进行堆排序*/
void HeapSort(int A[], int len)
{
BuildMaxHeap(A, len);
for (int i = len; i > 1; i--){
swap(A[i], A[1]);
HeadAdjust(A, 1, i - 1);
}
}
(2)效率及稳定性
(3)插入和删除
插入:放在队尾,再利用HeadAdjust进行调整。
删除:用队尾元素补齐,再利用HeadAdjust进行调整。
四、归并排序和基数排序
1.归并排序
(1)代码
/*内部排序利用二路归并*/
//int* B = (int*)malloc(n * sizeof(int));
int* B = new(n * sizeof(int));
//左右数组各自有序后,进行合并
void Merge(int A[], int low, int mid, int high) {
int i, j, k;
for (k = low; k <= high; k++)
B[k] = A[k];
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (B[i] <= B[j])
//A是最终数组,k表明即将带入的位置
A[k] = B[i++];
else
A[k] = B[j++];
}
while (i <= mid) A[k++] = B[i++];
while (j <= high) A[k++] = B[j++];
}
void MergeSort(int A[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
//分!
MergeSort(A, low, mid);
MergeSort(A, mid+1, high);
//合并的时候就顺便把顺序排了
Merge(A, low, mid, high);
}
}
(2)效率及稳定性
2.基数排序
(1)含义:
特殊的排序算法,因为不是先比整体大小,而是先把关键字拆分成d组,再整合。
(2)应用:
五、外部排序
1.难点:
数据元素太多,无法一次全部读入内存进行排序
2.思想:
使用“归并排序”的方法,最少只需在内存中分配3块大小的缓冲区(关键)即可对任意一个大文件进行排序【多路归并】
3.时间开销优化办法:尽量减少归并趟数(让开辟内存的缓冲区空间r更大)
关于归并段的数量:一共16块数据,进行生成初始归并段时,已经采用内部排序的办法让其成为8个有序的归并段,每个归并段已经是有序的了,所以只剩8个初始归并段(且每个段占两块)
关于读写磁盘次数:32+32*3=128 第一个32是指生成初始归并段,16段数据读入、写出各一次,一共32次,后面是因为将整片数据归并一趟,就需要花一个32,这里归并了3趟,所以需要3个32.
直观地说:读写外存时间与文件大小以及归并趟数有关,文件大小固定,所以为了减少读写次数就需要减少归并趟数,就必须加大每次读写的量,(每次抄的量大,看书的次数就变少)即不只采用2个输入缓冲区,而是采用4个缓冲区。
重要结论:采用多路归并可以减少归并趟数(K叉树)或者是减少初始归并段数量(初始归并段时采用多路归并使总段数变成8而不是16,r减少),从而减少磁盘I/O(读写)次数。
缺点:K叉树会导致内存开销增大且内部对比时间开销也会增大。
【解决办法:利用败者树减少关键字对比次数、利用置换-选择排序减少初始归并段数量】
3.败者树
若1024个元素对比,普通需要对比1023次,但是败者树只需要10次,即
4.置换-选择排序
(1)目的:仅仅依赖内部工作区大小,获得的归并段个数较多,且长度一致!为了探索新方法,获得更长的初始归并段,引入了置换-选择排序。
(2)含义:就是将最小的选出来,然后置换出去【但是置换的不能比之前的还小,否则冻结该窗口,所以要记录MINIMAX】。
利用空的内存工作区MA(Memory Area)以及初始归并段输出文件FO(Output File)来实现。
注意:在WA中选择MINIMAX记录的过程需要通过败者树来实现。
例:如果WA的容纳空间为6:
如果构成K叉树时缺少归并段怎么办,下面先给一个错误的例子:
正解如下:
注意:对于k叉归并,若初始归并段的数量无法构成严格的 k 叉归并树,则需要补充几个长度为 0 的“虚段”,再进行 k叉哈夫曼树的构造。
那么:在构造时是否需要补充虚段呢?补充虚段的个数又如何确定?
解:通过最佳归并树的规律来确定,主要是由于严格K叉树只包含度为K和0的结点(从这点看就知道第一个例子不正确)