【算法学习笔记】堆排序和归并排序、其他几种排序的代码实现、比较和应用(习题)

时间:2023-02-27 15:20:32


文章目录

  • ​​基本堆排序​​
  • ​​1.有20个数组,每个数组有500个元素,且是有序的,如何在20*500个数中找出排名前500的数​​
  • ​​设有两个递增的序列a,b 长度都为n,求前k个最小的a[i]+b[j]​​
  • ​​若要在N个海量数据(超过10亿,不能一次性放入内存)中找出最大的k个元素,(内存中可以容纳k个元素),最好采取什么数据结构和策略?​​
  • ​​归并排序​​
  • ​​设有一个整数序列,求数组中一共多少个逆序对​​
  • ​​快速排序​​

基本堆排序

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图
调整为大根堆:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。​

void shif(int a[],int low,int high)
{
int i = low,j = 2*i;//j为左孩子
int temp = a[i];
while(j <= high)
{
if(j < high && a[j] < a[j+1])//如果右孩子更大
j++;

if(temp < a[j]){//如果根节点小于最大孩子
a[i] =a[j];//将a[j]调整到双亲结点位置上
i = j;
j = 2*i;//继续向下调整
}
else break;//如果根节点大于等于最大的左右孩子
}
a[i] = temp;
}
void HeapSort(int a[],int n)
{
int i;
for(i = n/2;i >= 1;i--)
{
shif(a,i,n);//初始化循环建立堆

}
for(i = n;i >= 2; i--)//进行n-1趟堆排序,每一趟堆中元素个数减一
{
swap(a[i],a[1]);//将最后一个元素与a[1]交换
shif(a,1,i-1);//对a[1..i-1]进行筛选,得到i-1个结点的堆
}
}
int main() {
int a[6]={0,11,74,3,15,6};
HeapSort(a,5);
for(int i = 1;i <= 5;i++)
cout<<a[i]<<" ";
}
  • 每一趟通过堆调整产生有序区,它是全局有序区
  • 每次调整时间为o(log2n)
  • 产生初始堆的时间为o(n),比较次数约4n
  • 最好最坏和平均时间复杂度为o(nlog2n)
  • 是不稳定的排序算法

应用:STL中的priority_queue优先队列
一旦元素在优先队列容器中,删除操作将删除最高优先级元素

1.有20个数组,每个数组有500个元素,且是有序的,如何在20*500个数中找出排名前500的数

涉及知识点:

priority_queue中元素的比较 模板申明带3个参数:priority_queue<Type, Container, Functional>Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector。
比较方式默认用operator<,所以如果把后面2个参数缺省的话,优先队列就是大顶堆(降序),队头元素最大
仿函数本质就是类重载了一个operator(),创建一个行为类似函数的对象。

template <class T>
struct greater : public binary_function<T, T, bool> {
bool operator()(const T& x, const T& y) const { return x > y; }
};

题目思路:假设元素类型为int,有序数组的个数为max,利用堆结构,堆中结点类型为node,用vectormyv存储结果。
下面是假如有三个有序数组,找出前三个最小的元素的代码:

#include<stdio.h>
#include<vector>
#include<queue>
#define max 3
struct node{//小根堆的结点类型
int pn;//段号
int sn;//该段号中元素的序号
int data;//元素值
};
struct cmp{
bool operator() (const node& s1, const node& s2)const
{
return s1.data > s2.data;//创建小根堆
}
};

void Top(int *a[max],vector<int>&myv)
{
priority_queue<node, vector<node>, cmp>qu;//优先队列,注意其中的三个参数 这样每次出队时都是元素值最小的结点
struct node e;


for(int i = 0;i < max; i++)
{

e.pn = i;
e.sn = 0;
e.data = a[e.pn][e.sn];//将所有段的首元素入队
qu.push(e);
}
for(int i = 0;i < max; i++)
{
e = qu.top(); qu.pop();
myv.push_back(e.data);//元素值插入到vector中
e.sn++;//该段的下一个元素入队
e.data = a[e.pn][e.sn];

qu.push(e);
}
}

void dis(vector<int>p)
{
vector<int>::iterator it;
for(it = p.begin() ; it != p.end(); it++)
{
cout<< *it<<" ";
}
}

int main()
{
int a1[3] = {2,6,9};
int a2[3] = {-1,-1,5};
int a3[3] = {-3,2,4};
vector<int> my;
int *a[max];
a[0] = a1;
a[1] = a2;
a[2] = a3;
Top(a,my);
dis(my);
}

【算法学习笔记】堆排序和归并排序、其他几种排序的代码实现、比较和应用(习题)

设有两个递增的序列a,b 长度都为n,求前k个最小的a[i]+b[j]

与上一题类似,利用小根堆,先将a[0]+b[0]插入到小根堆qu中,当k>0时循环,出队元素e,插入到result中,k–,vector<node>result存放结果

void Top(int a[],int b[],int n,int k,vector<node>&p)//求前k个最小和
{
priority_queue<node>qu;//优先队列,注意跟上一题比较,这里只需要一个参数 这样每次出队时都是元素值最小的结点
struct node e,temp;
e.i = 0;
e.j = 0;
e.data = a[e.i]+b[e.j];
qu.push(e);

while(k)
{
e = qu.top(); qu.pop();
p.push_back(e);
k--;
if(e.j+1 < n){
temp.i = e.i;
temp.j = e.j + 1;
temp.data = a[temp.i++]+b[temp.j];
qu.push(temp);
}
if(e.j == 0 && e.i+1< n)//将a[e1.i+1]+b[e1.0]入队
{
temp.i = e.i+1;
temp.j = e.j;
temp.data = a[temp.i]+ b[temp.j];
qu.push(temp);
}



}
}

void dis(vector<node>p)
{

for(int i = 0;i < p.size(); i++)
{
cout<< p[i].data<<" ";
}
}

int main()
{
int a[3] = {2,4,5};
int b[3] = {1,2,6};
vector<node>vec;
Top(a,b,3,6,vec);

dis(vec);
}

注:在b没有扫描完时将a[e1.i]+b[e1.j+1]入队,当e.j = 0时还要将a[ei.i+1]+b[e1.0]入队

经过自己的试验,去掉e.j == 0 && 这句话,输出结果也是一样的

【算法学习笔记】堆排序和归并排序、其他几种排序的代码实现、比较和应用(习题)

若要在N个海量数据(超过10亿,不能一次性放入内存)中找出最大的k个元素,(内存中可以容纳k个元素),最好采取什么数据结构和策略?

首先读入k个元素,假设第一次读取的k个元素就是前k个最大元素,把k个元素建成小根堆,然后从第k+1个元素开始,读取的每个元素d都与堆顶元素进行比较,如果元素d大于堆顶元素,则把堆顶的元素替换成d,再将其调整为小根堆,当所有数据都读入并比较完之后,这个小根堆里面的所有元素就是最大的k个元素。

归并排序

分为自底向上的非递归过程和自顶向下的递归过程,每一趟产生的是局部有序区, 其最好 最坏 平均时间复杂度均为o(nlog2n)
是不稳定的排序算法。
空间复杂度为o(n)
(快速排序的空间复杂度是o(log2n)

设有一个整数序列,求数组中一共多少个逆序对

可以采用归并排序,在合并的过程中计算逆序数,这是一种高效的算法,时间复杂度远小于暴力解法。
设low<=i<=mid,mid+1<=j<=high,当a[i]<=a[j]时不产生逆序对,当a[i]>a[j]时逆序数有mid-i+1个(注意两半分别已经排好序了)

int sum;
void merge(int a[], int low, int mid, int high)
{
int i = low;
int j = mid + 1;
int *tmp = (int *) malloc((high - low +1)* sizeof(int));//分配足够空间
int k = 0;
while(j <= high && i <= mid)
{
if(a[i] > a[j])
{
sum += mid - i + 1;
tmp[k++] = a[j++];
}
else tmp[k++] = a[i++];
}
while(j <= high)
tmp[k++] = a[j++];
while(i <= mid)
tmp[k++] = a[i++];
for(int k1 = 0;k1 < k;k1++)
a[low+k1] = tmp[k1];//把tmp[0..k-1]复制到a[low..high]中
free(tmp);//释放空间
}

void merge_sort(int a[],int low,int high)
{
if(low < high){//注意一开始的条件判断
int mid = (low + high)/2;
merge_sort(a,low,mid);//先不断递归 得到最底层的数字 再合并
merge_sort(a, mid+1, high);
merge(a,low,mid,high);
}
}
int main()
{
int a[5] = {3,1,4,5,2};
merge_sort(a, 0, 4);
cout<<sum;
}

快速排序

快速排序之所以比较快,是因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样只能在相邻的数之间进行交换,交换的距离就大得多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的,都是O(N2),它的平均时间复杂度为O (NlogN)。其实快速排序是基于一种叫做“二分”的思想。

#include <stdio.h>
int a[101],n;//定义全局变量,这两个变量需要在子函数中使用
void quicksort(int left,int right)
{
int i,j,t,temp;
if(left>right)
return;
temp=a[left]; //temp中存的就是基准数
i=left;
j=right;
while(i!=j)
{
//顺序很重要,要先从右往左找
while(a[j]>=temp && i<j)
j--;
//再从左往右找
while(a[i]<=temp && i<j)
i++;
//交换两个数在数组中的位置
if(i<j)//当哨兵i和哨兵j没有相遇时
{
t=a[i];
a[i]=a[j];
a[j]=t;
}
}
//最终将基准数归位
a[left]=a[i];
a[i]=temp;
quicksort(left,i-1);//继续处理左边的,这里是一个递归的过程
quicksort(i+1,right);//继续处理右边的,这里是一个递归的过程
}
int main()
{
int i,j,t;
//读入数据
scanf("%d",&n);
for(i=1;i<=n;i++)
scanf("%d",&a[i]);
quicksort(1,n); //快速排序调用
//输出排序后的结果
for(i=1;i<=n;i++)
printf("%d ",a[i]);
getchar();getchar();
return 0;
}