基础算法
冒泡排序
两两元素依次进行比较,如果后面的元素比前面的小,说明顺序不对(这里是从最后开始往前进行),两个元素进行交换,循环进行,直到没有元素进行交换,结束循环
缺点:效率低 O(N^2)
#include<cstdio>
using namespace std;
//交换数组的中的元素,int b,int c为元素在数组中的下标
void Swap(int a[],int b,int c)
{
int temp = a[b];
a[b] = a[c];
a[c] = temp;
}
// int n为数组的大小
void BubbleSort1(int arr[],int n)
{
for (int i=0;i<n;i++)
{
for (int j=n-1;j>i;j--)
{
if (arr[j] < arr[j-1])
Swap(arr,j,j-1);
}
}
}
冒泡排序的优化
void BubbleSort2(int arr[],int n)
{
// flagw为循环标志,如果i下面没有发生交换
// 证明下面的顺序是对着的,不需要进行排序了
int flag = 1;
for (int i=0;i<n&&flag;i++)
{
flag = 0;
for (int j=n-1;j>i;j--)
{
if (arr[j] < arr[j-1])
{
Swap(arr,j,j-1);
flag = 1;
}
}
}
}
选择排序
通过N-i次关键字间的比较,从N-i+1个记录中选出关键字中最小的记录
效率低 O(N^2)
经常用它的内部循环方式求数组的最大值和最小值
#include<cstdio>
using namespace std;
//交换数组的中的元素,int b,int c为元素在数组中的下标
void Swap(int a[],int b,int c)
{
int temp = a[b];
a[b] = a[c];
a[c] = temp;
}
// 选择排序
// int n为数组的大小
void SelectSort(int arr[],int n)
{
int min;
for (int i=1;i<n;i++)
{
min = i;
for (int j=i+1;j<n;j++)
{
if (arr[j] < arr[min])
{
min = j;
}
}
if (i != min) Swap(arr,i,min);
}
}
插入排序
每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止
效率不高,O(N^2)
但是在序列基本有序时,它很快,他也有其适用范围
用循环实现
#include<cstdio>
using namespace std;
// 插入排序,用循环实现
void InsertSort(int arr[],int n)
{
// 因为数组arr[0]一个元素本身就是有序的,所以从arr[1]开始
// 将后面无序序列中的一个记录逐个插入到前面的有序序列中
for (int i=1;i<n;i++)
{
// 如果记录比有序序列最后一个元素小
if (arr[i] < arr[i-1])
{
// 将记录放置到辅助位置上
int temp = arr[i];
// 将记录与前一个元素逐个进行比较
for (int j=i-1;i>=0;i--)
{
// 如果记录比前一个元素小,交换位置
if (arr[j+1] < arr[j])
arr[j+1] = arr[j];
else
// 直到记录没有最后一个元素小,将记录放到当前位置
arr[j] = temp;
}
}
}
}
用递归实现
/**
* 这个插入排序时正确的
* int arr[] 为数组名
* int n 前有序数列的元素个数
* 将最后一个元素与前面已经排好顺序的数组做插入排序
*
*/
#include<cstdio>
using namespace std;
// 用递归实现插入排序
// 将第n个元素插入到前面的有序数列中
void insertsort(int arr[],int n)
{
// 就一个元素了,不需要再做比较了
if (n==1) return;
// 递归实现,将第n-1个元素插入到前面的有序数列中
insertsort(arr,n-1);
int x = arr[n-1]; // 最后一个元素的值
int index=n-2; // 前一个元素的“指针”
while(index>-1&&x<arr[index])
{
// 将元素后移一位
arr[index+1] = arr[index];
index--;
}
arr[index+1] = x;
}
希尔排序
前面我们知道,序列基本有序时或者记录数比较少时插入排序的效率还是很高的。而希尔排序就是插入排序的改进版本,又称缩小增量排序。
我们可以将原本有大量数据的序列进行分组,分成若干个小组后在各个小组内进行插入排序,这时各个小组就基本有序了,然后再对全体进行一次插入排序。
分组我们可以采取跳跃分割的方法:每次按n/2的间隔进行分组
效率准确为O(nlgn) - O(n^2) 有最好和最坏情况
效率大约为O(n^1.3)
#include<cstdio>
using namespace std;
// 希尔排序,int n为数组的大小
void ShellSort(int arr[],int n)
{
// 不断缩小的增量
for (int i=(n/2);i>=1;i=i/2)
{
// 插入排序
for (int j=i;j<n;j++)
{
int a=arr[j]; // 辅助元素
int index = j-i; // 同一小组元素的索引
while (index>-1&&a<arr[index])
{
arr[index+i] = arr[index];
index -= i;
}
// 因为循环内最后还执行了一次index-i,所以要加i
arr[index+i] = a;
}
}
}
分治法
快速排序
基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,则可以分别对这两部分记录记录继续进行排序,以达到整个序列有序的目的
就是使用分治法分而治之,选取一个主元,将数组左边的部分替换为比主元小的元素,右边为大的元素,在使用快速分别对比主元小的元素和大的元素进行排序,递归下去,最后这个序列就会变为有序的序列
快排是软件工业中最常见的常规排序法,其** 双向指针扫描 **和 **分区算法 **是核心,往往用于解决类似问题,特别partition用来划分不同性质的元素
单指针分区
#include<cstdio>
using namespace std;
// 交换元素函数
// int b,int c为所要交换的元素的数组下标
void Swap(int arr[],int b,int c)
{
int temp = arr[b];
arr[b] = arr[c];
arr[c] = temp;
}
// 单指针分区函数,函数返回主元在数组的下标
// int p,int r为元素位置,不是数组下标
int Partition(int arr[],int p,int r)
{
int povit = arr[p-1]; // 定义主元
int sp = p; // 定义与主元比较元素指针
int bigger = r-1; // 定义较大值的指针
while (sp<=bigger)
{
if (arr[sp]<=povit) { sp++; }
else { Swap(arr,sp,bigger); bigger--; }
}
Swap(arr,p-1,bigger);
return bigger;
}
// 快速排序函数
// int p为起始元素位置,int r为最后元素的位置
void QuickSort(int arr[],int p,int r)
{
if (p<r)
{
// k为主元在数组中的下标,从p开始
int k = Partition(arr,p,r);
QuickSort(arr,p,k);
QuickSort(arr,k+2,r);
}
}
双指针分区
// 对数字进行划分
// int p 为数组起始位置,int r 为数组结束位置
// 例如:从第一个元素排到第五个元素为QuickSort(a,1,5);
// 函数返回主元在数组中的下标
int Partition(int arr[],int p,int r)
{
int povit = arr[p-1];
int sp = p;
int bigger= r-1;
while (sp<=bigger)
{
if (arr[sp] <= povit) { sp++; }
else if (arr[bigger] > povit){ bigger--; }
else { Swap(arr,sp,bigger); }
}
Swap(arr,p-1,bigger);
return bigger;
}
// 对数组进行排序
// int p 为数组起始位置,int r 为数组结束位置的
// 例如:从第一个元素排到第五个元素为QuickSort(a,1,5);
void QuickSort(int arr[],int p,int r)
{
if (p<=r)
{
int k = Partition(arr,p,r);
QuickSort(arr,p,k);
QuickSort(arr,k+2,r);
}
}
三指针分区
struct Key
{
int equ;
int bigger;
}key;
// 对数字进行划分
// int p 为数组起始位置,int r 为数组结束位置
// 例如:从第一个元素排到第五个元素为QuickSort(a,1,5);
// 函数返回主元在数组中的下标
struct Key Partition(int arr[],int p,int r)
{
int povit = arr[p-1];
int sp = p;
int bigger= r-1;
int equ = p;
while (sp<=bigger)
{
if (arr[sp] < povit) { Swap(arr,sp,equ); sp++; equ++; }
else if(arr[sp] == povit) { sp++; }
else if (arr[bigger] > povit){ bigger--; }
else { Swap(arr,sp,bigger); }
}
Swap(arr,p-1,equ-1); equ--;
key.bigger = bigger;
key.equ = equ;
return key;
}
// 对数组进行排序
// int p 为数组起始位置,int r 为数组结束位置的
// 例如:从第一个元素排到第五个元素为QuickSort(a,1,5);
void QuickSort(int arr[],int p,int r)
{
if (p<=r)
{
Partition(arr,p,r);
QuickSort(arr,p,key.equ);
QuickSort(arr,key.bigger+2,r);
}
}
归并排序
将一个又n个元素的序列分成含有n/2个元素的子序列,再对两个子序列进行递归排序,将两个已经排好序的子序列进行合并
int helper[1024];
// 归并
void Marge(int arr[],const int p,int mid,const int r)
{
// 将arr中的数据拷贝到aa中的相同位置
memcpy(helper+(p-1),arr+(p-1),sizeof(int)*(r-p+1)); // 最关键的点
int left = p-1; // 左侧队伍的头部指针,指向带比较的元素
int right = mid; // 右侧队伍的头部指针,指向带比较的元素,mid位置所在的元素属于左部分
int current = p-1; // 原数组的指针,指向待填入数据的位置
// 不需要等于号,left、right为指针,mid、r为元素位置
while (left < mid && right < r)
{
if (helper[left] <= helper[right])
{
arr[current] = helper[left];
left++;
current++;
}
else
{
arr[current] = helper[right];
right++;
current++;
}
}
while (left<mid)
{
arr[current] = helper[left];
left++;
current++;
}
}
// 归并排序
// int p,int r为元素在数组中的位置,不是下标,使用时应该减
void MargeSort(int arr[],int p,int r)
{
if (p<r)
{
// int mid元素在数组中的位置,不是下标,使用时应该减一
int mid =p+((r-p)>>1);
MargeSort(arr,p,mid);
MargeSort(arr,mid+1,r);
Marge(arr,p,mid,r);
}
}
利用数据结构
堆排序
完全二叉树:初叶子结点外,每个结点都有两个子结点
堆是具有以下性质的完全二叉树:
每个结点的值都大于或等于其左右孩子值,称为大顶堆
每个结点的值都小于或等于其左右孩子值,称为小顶堆
用数组来实现堆(任意一个结点i,他的左孩子为2i+1,右孩子为2i+2)
堆排序(从小到大为例)
先将待排序的序列构造成一个大顶堆,那么堆顶的根结点就是最大值,将他与最末尾的元素交换,则最大值就到了尾部,我们再将剩下的n-1个序列按上面的步骤反复执行,就可以得到一个有序序列
先将序列堆化
就是将从n/2个结点开始,使每一个子树都为大顶堆
选取(9/2=4)第四个结点开始堆化,选取较小一个的第三个结点开始堆化,如果发生了交换,就要将交换时较小的那个值作为结点递归进行堆化
78大于任何一个孩子,不需要调整
4小于下面任何一个数,取下面最大的数进行交换
交换后取4所在的结点进行递归堆化,结点叶子结点,所以结束不用堆化
选第2个结点进行堆化,23与78交换
以23所在的位置进行递归堆化,23和43交换位置
23为叶子结点所以结束本次堆化,退会到结点1进行堆化
以结点1进行堆化时,2与78交换位置
以2所在的位置递归堆化
2与23进行交换,以2所在的位置递归堆化
2和23交换,以2所在的位置递归,2为叶子结点,堆化完成
推排序
将最前面与最后面的交换位置,再堆剩下的n-1个元素进行堆化,堆化后再堆排序,如此循环下去
将78与15进行交换,堆除25外的剩下8个元素进行堆化,堆排序
#include<cstdio>
using namespace std;
void Swap(int a[],int c,int d)
{
int temp;
temp = a[c];
a[c] = a[d];
a[d] = temp;
}
// 调整以i为根节点子树中的元素位置
// int i为子树根节点的下标,int n为数组的大小
void MaxHeapFixDown(int arr[],int i,int n)
{
int left = 2*i+1; // 左孩子
int right= 2*i+2; // 右孩子
// 左孩子已经越界,i为叶子结点
if (left >= n) return;
// 找到左右孩子中的最大值
int max = left;
// 右孩子越界,只剩下左孩子那左孩子必然就是最大值
if (right >= n) max = left;
else if (arr[right] > arr[max]) max = right;
// 如果arr[i]>arr[max],不用交换
if (arr[i] > arr[max]) return;
// 否则,将孩子中的最大元素与i交换
Swap(arr,i,max);
// 孩子中较大的元素位置发生了改变,递归调整子树
MaxHeapFixDown(arr,max,n);
}
// 堆化
// int n为数组的大小
void MaxHeap(int arr[],int n)
{
for (int i=(n/2-1);i>=0;i--)
MaxHeapFixDown(arr,i,n);
}
// 堆排序
void HeapSort(int arr[],int n)
{
// 堆arr进行堆化
MaxHeap(arr,n);
// 对堆的元素进行交换
for (int i=n-1;i>0;i--)
{
// 将堆顶的元素和最后一个元素交换
Swap(arr,0,i);
// 缩小堆的范围,对堆顶元素进行向下调整
MaxHeapFixDown(arr,0,i);
}
}
以上排序都是基于比较的排序,可证明它们在元素随机顺序情况下最好的是Nlg(N)
快排、归并、堆排的复杂度都是Nlg(N),其中表现最好的是快排,它是原址的不用开辟辅助空间,堆排也是原址的,但是堆排的常数因子较大,不具备优势
以下为非比较排序,在特定情况下会比基于比较的排序要快
计数排序
计数排序就是开辟一个辅助数组,将数组中的元素转换为辅助数组中的下标,假设元素均大于等于0,依次扫描原数组,将元素值记录在辅助数组的K为上,再依次扫描辅助数组,如果值为1,将下标的值插入到原原数组的对应位置。
计数排序属于用空间来换时间
如果用它解决问题时数列中的值分布非常广(最大值很大,元素分布很稀疏),空间将浪费很多
计数排序比较适用于–序列的关键字比较集中,已知边界且边界较小–
#include<cstdio>
#include<cstring>
using namespace std;
const int len = 1024;
int A[len];
// 计数排序,辅助空间为1024,所以只能对小于1024的数字排序
void CountSort(int arr[],int n)
{
memset(A,0,sizeof(int)*1024);
for (int i=0;i<n;i++)
{
A[arr[i]]++;
}
int k=0; //数据回填的位置
for (int j=0;j<len;j++)
{
while (A[j] > 0)
{
arr[k++] = j;
A[j]--;
}
}
}