代码参考《妙趣横生的算法.C语言实现》
本章总结查找和排序算法:顺序查找、折半查找、直接插入排序、冒泡排序、简单选择排序、希尔排序、快速排序、堆排序以及排序算法性能比较。
顺序查找就是在文件的关键字结合key[1,2,…n]中找出与给定的关键字key相等的文件记录。
步骤描述:
1、从文件的第一个记录开始,将每个记录的关键字与给定的关键字key进行比较
2、如果查找到某个记录的关键字等于key,则查找成功,返回该记录的地址。如果所有记录的关键字都与key进行了比较,但都未匹配,则本次查找失败,返回失败标记-1
//顺序查找:n表示记录个数、key表示要查找记录的关键字、key[]为存放所有记录关键字顺序表。
int sq_search(keytyped key[],int n,keytype key)
{
int i;
for (i=0;i<n;i++)
{
if (key[i] == key)
{
return i;
}
}
return -1;
}
用结构体描述:
typedef struct {
keytype key; //keytype类型的关键字key
datatype data; //记录其中信息
}RecordType;
int sq_search(RecordType r[], int n, keytype key)
{
int i;
for (i = 0;i < n;i++)
{
if (r[i].key == key) //查找成功
{
return i;
}
}
return -1;
}
//缺点:平均查找长度过大,查找效率较低
只有在关键字的排序是有序的(递增或递减)情况下,才能应用折半查找的算法描述。
基本思想:
减少查找序列的长度,分而治之进行关键字的查找。
查找过程:先去定待查找记录的所在反胃,然后逐渐缩小查找的范围,直到找到为止(也可能查找失败)
//基于递增序列的折半查找
//n表示记录个数、k表示要查找到的关键字、key[]关键字顺序表
int bin_search(keytype key[], int n, keytype k)
{
int low = 0, high = n - 1, mid;
while (low <= high)
{
mid = (low+high) / 2;
if (key[mid] == k)
return mid;
if (k > key[mid])
low = mid + 1; //在后半序列中查找
else
high = mid - 1;
}
return -1; //查找失败,返回-1
}
用结构体描述:
typedef struct {
keytype key; //关键字
datatype data; //记录的信息
}RecordType;
int bin_search(RecordType r[], int n, keytype key)
{
int low = 0, high = n - 1, mid;
while (low <= high)
{
mid = (low + high) / 2;
if (r[mid].key == key)
return mid;
if (key > r[mid].key)
low = mid + 1; //在后半序列中查找
else
high = mid - 1;
}
return -1; //查找失败,返回-1
}
排序可以理解为:
根据文件记录的关键字值得递增或者递减关系将文件记录的次序进行重新排列的过程。
或者是:将一个按值无序的数据序列转换成为一个按值有序的数据序列的过程
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,其基本操作是将一条记录插入到已排好的有序表中,从而得到一个新的、记录数量增1的有序表。
在日常生活中,经常碰到这样一类排序问题:把新的数据插入到已经排好的数据列中。例如:一组从小到大排好顺序的数据列{1,2,3,4,5,6,7,9,10},通常称之为有序列,我们用序号1,2,3,…表示数据的位置,欲把一个新的数据8插入到上述序列中。
完成这个工作的步骤:
①确定数据“8”在原有序列中应该占有的位置序号。数据“8”所处的位置应满足小于或等于该位置右边所有的数据,大于其左边位置上所有的数据。
②将这个位置空出来,将数据“8”插进去。
直接插入排序(straight insertion sort)的做法是:
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环。
#include
using namespace std;
void Insertsort(int a[],int k)
{
int i, j;
for (i = 1;i < k;i++)//循环从第2个元素开始
{
if (a[i] < a[i - 1])
{
int temp = a[i];
for (j = i - 1;j >= 0 && a[j] > temp;j--)
{
a[j + 1] = a[j];
}
a[j + 1] = temp;//此处就是a[j+1]=temp;
}
}
}
int main()
{
int a[] = {
98,76,109,34,67,190,80,12,14,89,1 };
int k = sizeof(a) / sizeof(a[0]);
Insertsort(a,k);
for (int f = 0;f < k;f++)
{
cout << a[f] << " ";
}
return 0;
}
基本思想:第i趟排序从序列后n-i+1个元素中选择一个最小的元素,与该n-i+1个元素的最前面那个元素进行位置交换,也就是与第i个位置上的元素进行交换,直道n=i-1;
直观讲,每一趟的选择排序就是从序列中未排好顺序的元素中选择一个最小的元素,将钙元素与这些未排好的元素中的第一个元素交换位置。
void Selectsort(int a[], int k)
{
int i, j,min;
int tmp;
for (i = 0;i < k;i++)
{
min = i;
for (j = i + 1;j < k;i++)
{
if (a[j] <= a[min])
{
min = j;
}
}
if (min != i) //如果找到比a[min]还要小的值就进行交换位置,否则不交换
{
tmp = a[min];
a[min] = a[i];
a[i] = tmp;
}
}
}
基本思想描述:
1、将序列中的第一个元素和第二个元素进行比较,若前者大于后者,则将第一个元素与第二个元素进行位置交换,否则不交换
2、将第2个元素与第3个元素进行比较,同样若前者大于后者,则将第2个元素与第3个元素进行位置交换,否则不交换。
3、以此类推,直到将第n-1个元素与第n个元素进行比较为止。此过程称为第1趟冒泡排序,进过第21趟冒泡排序后,将长度为n的序列中最大的元素置于序列的尾部,即第n个位置上。
4、之后再进行第2趟…第n-1趟排序。冒泡排序完成。
改进思路:
以序列3 6 4 2 11 10 6为例:
第一趟bubblesort之后:
3 4 2 6 10 6 11
第二趟bubblesort之后:
3 2 4 6 6 10 11
第三趟bubblesort之后:
2 3 4 6 6 10 11
这时再进行冒泡排序就会发现,序列本身不会再发生变化,只有相邻元素的比较,而没有相邻元素的交换,也就是说此时排序已经完成了。
所以可以这样改进:
如果某一趟排序过程中只有元素之间的比较而没有元素之间的位置交换,说明排序完成。
void ImprovedBubblesort(int a[], int n)
{
int i, j;
int tmp;
int flag = 1; //flag=1,说明本趟排序中仍有元素交换动作
for (i = 0;i < n && flag==1;i++) //趟次,一共n-1次
{
flag = 0;
for (j = 0;j < n - 1;j++) //元素交换
{
if (a[j] > a[i])
{
flag = 1;
tmp = a[j];
a[j] = a[i];
a[i] = tmp;
}
}
}
}
基本思路:
1、设定一个元素间隔增量gap,将参加排序的序列按照这个间隔数gap从第1个元素开始依次分成若干个子序列。
2、在子序列中可以采用其他的排序方法,例如冒泡排序。
3、缩小增量gap,重新将整个序列按照新的间隔数gap进行划分,再分别对每个子序列排序。过程描述:缩小增量gap–>划分序列–>将子序列排序
4、直到间隔数gap=1为止。
希尔排序过程:
思考:如何确定间隔数gap?
数学上仍然是一个尚未解决的难题,但是经验告诉我们一种比较常用且效果好的方法:
1、首先gap取值为序列长度的一半
2、后续排序过程中,后一趟排序的gap取值为前一趟排序gap的一半取值
算法描述:
void Shellsort(int a[], int n)
{
int i, j;
int tmp;
int flag = 1;
int gap = n; //第一次gap为n
while (gap > 1)
{
//确定gap
gap = gap / 2;
//以gap划分子序列,每一次迭代都是一组子序列的一趟自排序(冒泡)
do {
flag = 0;
for (i = 0;i < n - gap;i++)
{
j = i + gap;
if (a[j] < a[i])
{
flag = 1;
tmp = a[j];
a[j] = a[i];
a[i] = tmp;
}
}
} while (flag==1);
}
}
基本思想:
1、在当前的排序序列中任意选取一个元素,把该元素称为基准元素或支点,把下雨等于基准元素的所有元素都移动到基准元素的前面,把大于基准元素的所有元素都移到基准元素的后面,这样使得基准元素所处的位置 恰好就是排序的最终位置,并且把当前参加排序的序列分为前后两个序列。
2、上述的过程称为一趟快速排序,即快速排序的一次划分
3、接下来分别对这两个子序列重复上述的排序操作(如果子序列长度大于1的话),直到所有元素都被移动到排序后他们应处的最终位置上。
效率之所以高:每一次元素的移动都是跳跃的,不会像冒泡排序只能在相邻元素之间进行,元素移动的间隔较大,因此总的比较和移动次数减少
具体步骤:
1、假设序列a,设置两个变量i、j.分别指向首元素和尾元素,设定i指向的首元素为基准元素
2、反复执行i++,直到i指向的元素>=基准元素,或者i指向尾部
3、反复执行j–,直到指向的元素<基准元素,或者j指向头部
4、若此时i 5、完成第一次交换后,重复执行步骤1、2,直到i>=j位置 6、此时i>=j,然后将基准元素与j指向的元素交换位置,至此完成了原序列的第一次划分 7、接下来分别对基准元素前后的子序列中长度大于1的子序列重复执行上述操作。 步骤分析: 对于每个子序列的操作又是一次划分,因此这个算法具有递归性质。 每次划分过程的基准元素仍可设定为子序列的第一个元素 快速排序只适用于顺序表线性结构或者数组序列的排序,不适合在链表上实现 heapsort是选择排序的改进。 首先了解一下堆的概念: 堆通常是一个可以被看做一棵完全二叉树的数组对象。 堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。 (ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2) 堆总是满足下列性质: 1、 堆中某个节点的值总是不大于或不小于其父节点的值; 2、 堆总是一棵完全二叉树。 了解一下完全二叉树的概念: https://blog.csdn.net/judgejames/article/details/87868602 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆 举例: 序列{49,22,40,20,18,36,6,12,17} 1、将原始序列构成一个堆(建立初始堆) 2、交换堆的第一个元素和堆的最后一个元素 3、将交换最大值元素之后的剩余元素所构成的序列再转换成一个堆 4、重复上述2、3步骤n-1次 经过上述操作,就可以将一个无序的序列从小到大进行排序。 关键问题: 1、如何原始序列构成一个堆 2、如何将交换最大值元素之后的剩余元素所构成的序列再转换成一个堆 第二个问题: 该二叉树虽然不是一个堆,但是除了根结点外,其余任何一棵子树仍然满足堆的特性。 自上而下调整:将序号为i的结点与其左右孩子(2i 、2i+1)三个值中的最大值替换到序号为i的结点的位置上。 只要彻底地完成一次自上而下的调整,该二叉树就会变成一个堆。 最后一步:交换第一个元素和新堆的最后一个元素的位置,也就是将最大的元素移至新堆的最后。 第一个问题: 如果原序列对应的完全二叉树有n个结点,则: 1、初始化时,令序列号为i= Floor (n/2),它对应于而擦函数中第 Floor (n/2)个结点(二叉树中的结点按照层次编号,从1开始,从左到右,从上到下编号) 2、调用函数adjust调整 3、每执行完一次调整都执行一次i=i-1操作 4、重复步骤2、3,直到i==1时执行步骤5 5、最后再调用adjust函数调整一次。 这样就完成调整了。 示例:初始序列为:{23,6,77,2,60,10,58,16,48,20} 需要把握的要点: 1、堆排序是针对线性序列的排序,之所以要采用完全二叉树的形式解释堆排序的过程,是出于方便解释的需要 2、堆排序的第一步是将原序列变成一个对序列 3、一系列的交换调整操作。所谓交换就是将堆中第一个元素与本次调整范围内的新堆的最后一个元素交换位置,使得较大的元素能够置于序列的最后面,所谓调整就是将交换后的剩余元素从上至下调整为为一个新堆的过程。 4、通过2、3操作可以将一个无序序列从小到大偶爱徐 5、如果基于大顶堆进行堆排序,则排序后的序列从小到大。若是基于小顶堆,则从大到小。 总结: 1、如果参加排序的序列最开始就是基本有序或者局部有序的,使用这直接插入排序和冒泡排序的效果较好,排序速度较快,最好的情况下(原序列按值有序),时间复杂度O(n) 2、快速排序最快,堆排序空间消耗最小 3、序列中元素个数越小,采用冒泡排序排序算法、直接插入排序、简单选择排序较合适 当序列规模变大时,采用希尔排序、快速排序和堆排序比较合适 4、从稳定性来讲:直接插入、冒泡是稳定的排序方法。简单选择排序、希尔排序、快速排序、堆排序是不稳定的排序算法//快速排序
void Quicksort(int a[], int s,int t)
{
int i, j;
if (s < t)
{
//【1】设置两个变量i、j.分别指向首元素和尾元素,设定i指向的首元素为基准元素
i = s;
j = t + 1;
while (1)
{
do i++;
while(!(a[s]<=a[i] || i==t)); //【2】重复i++操作,直到i指向的元素>=基准元素,或者i指向尾部
do j--;
while (!(a[s]>=a[j] || j==s)); //【3】反复执行j--,直到指向的元素<基准元素,或者j指向头部
if (i < j) //【5】若此时i
8、堆排序
基于大顶堆的完全二叉树表示的堆排序的核心思想可描述如下:
code://堆排序
//【1】将二叉树调整为一个堆的函数:
//函数作用:将以第i个元素作为根结点的子树调整为一个新的堆序列,前提是该子树中除了根结点外其余的任何一个子树仍然满足堆的特性,如果该子树除了根结点外其他子树也不完全是
//堆结构的话,则不能仅通过依次调用adjust函数就将其调整为堆
//输入:序列a i:序列a中的元素下标
void BiTreeAdjustToHeap(int a[],int i,int n)
{
int j;
int tmp;
tmp = a[i];
j = 2 * i; //j为i的左孩子结点序号
while (j <= n)
{
if (j < n && a[j] < a[j + 1])
{
j++; //j为i的左右孩子中较大孩子的序号
}
if (tmp >= a[j]) //如果父结点值比孩子值还大就不需要调整了
{
break;
}
a[j / 2] = a[j]; //较大的子节点与父节点交换位置
j = 2 * j; //继续向下调整
}
a[j / 2] = tmp;
}
//【2】原始序列初始化函数
void InitHeap(int a[],int n)
{
for(int i=n/2;i>=0;i--)
{
BiTreeAdjustToHeap(a,i,n);
}
}
//堆排序函数
void Heapsort(int a[], int n)
{
int i = 0;
//【1】原始序列初始化函数
InitHeap(a,n);
//【2】交换第1个和第n个元素,再将根结点向下调整
for (i = n - 1;i >= 0;i--)
{
swap(a[i+1],a[0]);
BiTreeAdjustToHeap(a,0,i); //将根结点向下调整
}
}
9、排序算法性能比较
排序算法
平均时间
最坏情况
空间需求
直接插入排序
O(n^2)
O(n^2)
O(1)
冒泡排序
O(n^2)
O(n^2)
O(1)
简单选择排序
O(n^2)
O(n^2)
O(1)
希尔排序
O(nlog2n)
O(nlog2n)
O(1)
快速排序
O(nlog2n)
O(n^2)
O(nlog2n)
堆排序
O(nlog2n)
O(nlog2n)
O(1)
10、所有算法的code(C语言)
#include