
插入排序
直接插入排序
基本思想:
将待排序的序列中的每一个元素插入到一个已排好序的序列中,使用数组第一个位置或者工作指针,来存放待插入的元素,待插入的元素从后往前和有序区的每个元素比较,将>=自己的全部向后移动(链表不需要)最后放入自己的位置。
适合条件:
序列中的记录已经一基本有序或者排序记录较少
当排序记录个数较多时,大量的比较和移动操作使得其效率很低
代码
顺序表实现:
//简单插入排序
void InsertSort(int r[],int n)
{
int i, j;
for ( i = 2; i < n; i++) //从2开始,0号单元用来暂存单元和哨兵
{
r[0] = r[i];
for ( j = i-1 ; r[0]<r[j] ; j--) //寻找插入位置
{
r[j + 1] = r[j]; //>=待插入元素的都要向后移动
}
r[j + 1] = r[0];
}
}
单链表实现:
单链表思路为将有序区放在链表的最后,这样方便向后查找;
同时在有序区的第一位前放置一个空节点来定位
无序区只需要用first头指针来定位即可
使用p工作指针来寄存(或者说摘除使用)待比较元素,每次遍历插入前先将头节点后的节点从链中摘下,去无序区比较直接插入
需要注意的是:有序区用于比较的工作指针是用当前比较位置的前一位的位置的指针来操作,这样才能方便后续的插入节点 但是这样可能会出现该指针访问后续节点时出现引用NULL的情况,可以while ( r->next != NULL&&p->data > r->next->data)(使用&&运算符性质,防止其引用空指针)
//简单插入排序(单链表)
//这里将有序区放在后面,遇到就直接插入
void LinkList::InsertSort()
{
Node* p = first->next;
Node* q = p, * r;
while (q->next->next)//q定位到倒数第二个节点
q = q->next;
//在倒数第二个节点后插入一个空节点
r = new Node;
r->next = q->next;
q->next = r;
q = r;
while (first->next != q) {
p = first->next;
first->next = p->next; //将p节点从链中摘除
r = q;
while ( r->next != NULL&&p->data > r->next->data)//这里通过&&的运算性质可以避免r->next为空在后面使用空指针的情况
{
r = r->next;
}
p->next = r->next; //在寻找到的位置插入
r->next = p;
}
//删除空节点
first->next = q->next;
delete q;
}
运行
测试代码
int main() {
int a[11] = { 0,14,58,96,52,32,14,63,20,40,7 };
for (int i = 1; i <= 10; i++)
{
cout << a[i] << '\t';
}
cout << endl;
InsertSort(a, 10);
for (int i =1; i <= 10; i++)
{
cout << a[i] << '\t' ;
}
cout << endl;
int b[10] = { 14,58,96,52,32,14,63,20,40,7 };
LinkList li(b, 10);
li.printList();
li.InsertSort();
li.printList();
return 0;
}
希尔排序
算法描述:
由于直接插入排序只在基本有序或比较短的空间排序效率高,
现解决直接插入排序的两个问题:
- 适合较短序列
在直接插入排序的基础上若将序列分为较短的多个序列
- 适合基本有序序列
每个短序列不连续,每个序列的各个元素广泛的分布在各个位置,使得每次短序列排序能够使整个序列基本有序,而不是局部有序,这样可将其推广到更长的序列排序中
代码
void shellSort(int r[],int n)
{
int d, i, j;
for (d=n/2;d>=1;d=d/2) //多个短序列循环整合
{
for ( i = d; i <= n; i++) //每个序列内循环插入每个元素
{
r[0] = r[i]; //存放待插入元素
for (j = i-d; j>0 && r[0] <r[j]; j-=d) //寻找插入位置
{
r[j + d] = r[j];
}
r[j + d] = r[0];
}
}
}
运行
测试代码
int main()
{
int a[11] = { 0,59,20,17,36,98,14,23,83,13,28 };
for (int i = 1; i <=10; i++)
{
cout << a[i] << '\t';
}
cout << endl;
shellSort(a, 10);
for (int i = 1; i <= 10; i++)
{
cout << a[i] << '\t';
}
cout << endl;
return 0;
}
交换排序
冒泡排序
算法描述:
- 两两比较相邻的元素,每一趟将最值移到当前最高位,所以叫做冒泡
- 用一个bound边界值来判定每趟比较的边界(每次冒泡之后就不需要再比较到最后的位置,只需要到已排好的最值之前就可)
- 边界定于上一趟最后的交换位置
- 若上一趟没有交换,则说明已经完成排序,直接退出
代码
void bubbleSort(int r[], int n)
{
int exchange = n, bound;
while (exchange!=0)
{
bound = exchange; //每次bound设置为上一次较小的交换位置
exchange = 0;
for (int j= 0; j <bound; j++) //注意这里没有等于(之前的排序方式都会是等于)
{
if (r[j] > r[j + 1]) {
exchange = r[j];
r[j] = r[j + 1];
r[j + 1] = exchange;
}
exchange = j;
}
}
}
值得注意的是:
循环结束判定位置没有等于号,因为j+1会访问最后一个元素,所以循环只能结束在倒数第二个。注意这里测试的数组都为11个元素大小
exchange必须要留而不能只留下一个bond的原因在于exchange在每次交换后都会改变,但是bound边界在一趟冒泡中不能改。同时exchange可以充当临时变量,不用新建一个temp
运行
测试代码
void main()
{
int a[11] = { 0,59,20,17,36,98,14,23,83,13,28 };
for (int i = 1; i <= 10; i++)
{
cout << a[i] << '\t';
}
cout << endl;
bubbleSort(a, 10);
for (int i = 1; i <= 10; i++)
{
cout << a[i] << '\t';
}
cout << endl;
}
快速排序
算法描述:
- 一次划分例子:这种方式会不断改变轴值位置,最后在正确位置
- 将冒泡排序的相邻元素相比较和冗余的多次交换升级为直接交换位置
- 分而治之:划分为多次交换,从大到小,深入递归
- 适用于待排序记录个数很大且原始数据排列随机情况,不稳定,迄今为止最好排序之一
代码:
改变轴值方式:
int Partition(int r[ ], int first, int end)
{
int i = first, j = end; //初始化
while (i < j)
{
while (i < j && r[i] <= r[j]) j--; //右侧扫描
if (i < j)
{
r[0] = r[i]; r[i] = r[j]; r[j] = r[0];
i++;
}
while (i < j && r[i] <= r[j]) i++; //左侧扫描
if (i < j)
{
r[0] = r[i]; r[i] = r[j]; r[j] = r[0];
j--;
}
}
return i; //i为轴值记录的最终位置
}
void QuickSort(int r[ ], int first, int end)
{
if (first < end)
{ //区间长度大于1,执行一次划分,否则递归结束
int pivot=Partition(r, first, end); //一次划分
QuickSort(r, first, pivot - 1); //递归地对左侧子序列进行快速排序
QuickSort(r, pivot + 1, end); //递归地对右侧子序列进行快速排序
}
}
轴值最后归位方式
void quicksort( int a[],int left, int right)
{
int i, j, t;
i = left;
j = right;
t = a[left];
if (left > right) return;
while (i != j)
{
while (a[j] >= t && j > i) j--;
while (a[i] <= t && j > i) i++;
if (i < j)
{
a[0]=a[i];a[i]=a[j];a[j]=a[0];
}
}
a[left] = a[i];
a[i] = t;
quicksort(a,left, --i);//递归左子序列
quicksort(a,++j, right);//递归右子序列
return;
}
return;
}
注意:
j- -和i++的顺序不能互换,这样可以保证i和j相遇时,所指的位置为比轴值小的数字,这样最后交换的时候才能使轴值左边的数都比自己小,右边的都比自己大,趋于有序
运行
测试代码
int main()
{
int a[101];
int i, j;
srand(time(0));
for (i = 1; i <=100; i++)
a[i] = rand() % 101; //生成[0,100]的随机数,并给a;
quicksort(a,1, 100);
for (i = 0; i <100; i++)
{
cout << a[i] << '\t';
if ((i + 1) % 10 == 0) cout << endl;
}
}
选择排序
简单选择排序
算法描述:
是直接插入排序和冒泡排序的结合,将数组分为有序区和无序区,同时每一趟选择出无序区中的最小值,直接放在有序区的最后一位
代码
void selectSort(int r[ ], int n)
{
for (int i = 1; i < n; i++) //n-1趟简单选择排序
{
int index = i; //index记录最小值下标(每次从无序区的第一位开始)
for (int j = i + 1; j <= n; j++) //在无序区中选择最小值
if (r[j] < r[index]) index = j;
if (index != i)
{
r[0] = r[i]; r[i] = r[index]; r[index] = r[0];
}
}
}
运行
测试代码同上,只是修改函数
堆排序
算法描述:
- 堆的概念:
堆是一种逻辑上为完全二叉树的序列,实现方法用数组,分为两种
大根堆:每个节点的值都<=左右孩子节点的值
小根堆:每个节点的值都>=左右孩子节点的值
例如:大根堆{47,35,26,26,18,7,13,19}
- 基本思路
将待排序序列构造成一个大根堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,再将其放入n-1位置。以此类推,便能得到一个有序序列了
每次建堆后,堆的根为序列的1号位置,所以要将其与当前比较趟次的末尾交换,并再次建堆
- 堆排序对原始记录的排列方式不敏感,这是其相较于快速排序的最大优点
但是,虽然两者的时间复杂度相同,但是当数据量较多时,快速排序明显优于堆排序,堆排序每次重构堆的时候会有一些多余的交换,同时快速排序的访问方式为顺序(++ --顺序)而堆排序多为跳跃式(i*2)在数据量很大时也会变慢。 - 堆排序讲的更好的: 链接
代码
//注意这里的建堆只是根节点不为最大值,其左右两孩子已经视为构成了堆
//在后面会从后面开始建堆,最后从下向上建成堆
void Sift(int r[ ], int k, int m)
{
int i = k, j = 2 * i; //i指向被筛选结点,j指向结点i的左孩子
while (j <= m) //到该次排序序列的最末尾结束
{
if (j < m && r[j] < r[j+1]) j++; //比较i的左右孩子,j指向两者中的较大者
if (r[i] > r[j]) break; //根结点已经大于左右孩子中的较大者
else
{
r[0] = r[i]; r[i] = r[j]; r[j] = r[0]; //将根结点与结点j交换
i = j; j = 2 * i; //被筛结点位于原来结点j的位置
}
}
}
void HeapSort(int r[ ], int n)
{
int i = 0;
for (i = n/2; i >= 1; i--) //初始建堆,从最后一个分支结点至根结点
Sift(r, i, n);
for (i=1; i<n; i++) //重复执行移走堆顶及重建堆的操作
{
r[0] = r[1]; r[1] = r[n - i + 1]; r[n - i + 1] = r[0];
Sift(r, 1, n-i);
}
}
运行
测试代码同上,只是修改函数
归并排序
算法描述:
类似希尔排序,只是先划分为多个较小的组内让其局部有序,再多个组整合。
每组局部有序之后归并时,在归并的两个小组最前端设立索引,将其归并为新的小组时每次比较前端的元素,将较小者放在前面
进行一次归并的分片图(两边不一定相等),左边从[i1,m] ,右边[j,t],每次比较a[i1]和a[j],将小的放入resu数组。
//一次归并一组数据(两边不一定要相等)
void merge(int a[], int resu[], int s,int m, int t)
{
int i1 = s, j = m + 1, i2 = s;//i1指向数组a[],i2指向resu[]
while (i1<=m&&j<=t)
{
if (a[i1]<a[j])
resu[i2++] = a[i1++];
else
resu[i2++] = a[j++];
}
if (i1 <= m)
while (i1 <= m) //若第一个序列还没完
resu[i2++] = a[i1++];
else
while(j<=t) //若第二个序列还没完
resu[i2++] = a[i2++];
}
进行一趟完整的归并:
以长度h划分一个小组, 每两组归并一次。进行归并时的位置判断有三种情况,如下
- i没有进入最后区域,即i<=n-2h+1,这样i只要向后移动正常归并就可i+=2h;
- 所有小组的个数为偶数,i进入了最后区域,但i<=n-h+1,这时最后部分有一个长度为h的小组和一个长度<h的小组,只需要将他俩整合即可。
- 所有小组的个数为奇数,只剩下了一个长度<h的小组,这时直接将该有序序列放入最后结果即可
//一趟归并
void mergePass(int a[], int resu[], int n, int h)
{
int i = 1;
while (i<=n-2*h+1)
{
merge(a, resu, i,i + h - 1, i + 2 * h - 1);
i += 2 * h;
}
if (i < n - h + 1)
{
merge(a, resu, i, i + h - 1, n);
}
else
for (int j = i; j < j<=n; j++)
{
resu[j] = a[j];
}
}
完整的归并:
- 非递归算法
将a和resu轮流当作容器存放结果,一趟一趟归并,最终整合到resu数组中。自底向上分治,效率高于递归方法。
//非递归算法,将a和resu轮流当作容器存放结果
void mergeSort1(int a[], int resu[], int n)
{
int h = 1;
while (h<n)
{
mergePass(a, resu, n, h);
h = h*2;
mergePass(resu, a, n, h);
h = h * 2;
}
}
- 递归实现
将序列划分为两个序列,分别进行归并排序,最后整合为一个序列。自顶向下分治,但效率不如非递归。
void mergeSort2(int a[], int resu[], int s,int t)
{
int m;
if (s == t)//待排序序列只剩了一个元素
{
resu[s] = a[s];
}
else
{
m = (s + t) / 2;
mergeSort2(a, resu, s, m);//归并前半个子序列
mergeSort2(a, resu, m+1, t);//归并后半个子序列
merge(resu, a, s, m, t);//归并两个传入的序列
}
}
to be continued·····