常用的查找与排序算法
目录
-
工具
1.生成随机数组
- rand() 的内部实现是用线性同余法做的,它不是真的随机数,因其周期特别长,故在一定的范围里可看成是随机的。
- rand() 返回一随机数值的范围在 0 至 RAND_MAX 间。RAND_MAX 的范围最少是在 32767 之间(int)。用 unsigned int 双字节是 65535,四字节是 4294967295 的整数范围。0~RAND_MAX 每个数字被选中的机率是相同的。
- rand() 产生的是伪随机数字,系统默认的随机数种子为 1。每次执行时是相同的; 若要不同, 用函数 srand() 初始化它。
void generateArr(int arr[],int n,int rangL,int rangR)
{
//随机数种子
srand(time(NULL));
for(int i=0;i<n;i++)
arr[i]=rand()%(rangR-rangL-1);
}
int main()
{
std::cout<<"please input num: "<<std::endl;
int n;
cin>>n;
int arr[n];
generateArr(arr,n,5,100);
for(int i=0;i<n;i++)
{
std::cout<<arr[i]<<" ";
}
std::cout<<std::endl;
return 0;
}
2.测试排序算法的运行时间
void test_sort(string sort_name,void(*sort)(int [],int),int arr[],int n)
{
clock_t star_time=clock();
sort(arr,n);
clock_t end_time=clock();
std::cout<<sort_name<<" time: "<<double(end_time-star_time)/CLOCKS_PER_SEC<<std::endl;
}
3.打印输出
void PrintArray(int *arr,int n)
{
for(int i=0;i<n;i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
-
排序
1.冒泡排序
-
原理
每一轮,每两个元素比较大小并进行交换,直到这一轮当中最大(最小)的元素被放置在数组的尾部,然后不断地重复这个过程,直到所有元素都排好位置。其中,核心操作就是元素相互比较。
-
实现
void bubble_sort(int arr[],int n)
{
for(int i=0;i<n-1;i++)
{
for(int j=0;j<n-1-i;j++)
{
if(arr[j+1]<arr[j])
{
swap(arr[j+1],arr[j]);
}
}
}
}
- 优化版本: 判断前一轮中是否进行过交换,如果没有,则表示已经有序了;否则,为无序的。
void BubbleSort2(int* num,int n)
{
int temp;
int flag=1;//无序标志位
for(int i=0;i<n-1&&flag==1;i++)
{
flag=0;
for(int j=0;j<n-1-i;j++)
{
if(num[j]>num[j+1])
{
temp=num[j];
num[j]=num[j+1];
num[j+1]=temp;
flag=1;
}
}
}
}
-
复杂度
- 最好的情况:数组已排序好
需要进行n−1次的比较,两两交换次数为O(1),时间复杂度是O(n)。
- 最坏的情况:数组逆序排列
需要进行 n(n-1)/2 次比较,时间复杂度是 O(n^2)。
- 一般情况:
平均时间复杂度是 O(n^2)。
2.选择排序
-
原理
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
-
实现
void select_sort(int arr[],int n)
{
int mindex;
for(int i=0;i<n;i++)
{
mindex=i;
for(int j=i+1;j<n;j++)
{
if(arr[mindex]>arr[j])
{
mindex=j;
}
}
swap(arr[mindex],arr[i]);
}
}
-
复杂度
- 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。
- 唯一的好处可能就是不占用额外的内存空间了吧。
3.插入排序
-
原理
-
实现
template <typename T>
void sort_insert(T arr[],int n)
{
for(int i=1;i<n;i++)
{
for(int j=i;j>0;j--)
if(arr[j]>arr[j-1])
swap(arr[j],arr[j-1]);
else
break;
}
}
- 优化版本:
template <typename T>
void sort_insert_1(T arr[],int n)
{
for(int i=1;i<n;i++)
{
T e=arr[i];
int j;//终止的位置
for(j=i;j>0;j--)
{
if(arr[j-1]>e)
arr[j]=arr[j-1];
else
break;
}
arr[j]=e;
}
}
4.归并排序
-
原理
归并排序(merge sort):利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
归并排序分为三个过程:
- 将数列划分为两部分(在均匀划分时时间复杂度为 );
- 递归地分别对两个子序列进行归并排序;
- 合并两个子序列。
具体实现:
定义 mergeSort(nums, l, r) 函数表示对 nums 数组里 [l,r]的部分进行排序,整个函数流程如下:
- 递归调用函数 mergeSort(nums, l, mid) 对 nums 数组里 [l,mid]部分进行排序。
- 递归调用函数 mergeSort(nums, mid + 1, r) 对 nums 数组里 [mid+1,r]部分进行排序。
- 此时 nums 数组里 [l,mid]和 [mid+1,r]两个区间已经有序,我们对两个有序区间线性归并即可使 nums 数组里 [l,r] 的部分有序。
- 我们对nums[l,r]里的数据进行复制一份,构建一个辅助数组,存在l的偏移量。
- 由于两个区间均有序,所以我们维护两个指针 i和 j表示当前考虑到 [l,mid]里的第 i个位置和 [mid+1,r]的第 j个位置。
- 如果 nums[i] < nums[j] ,那么我们就将 nums[i]放入数组 nums[l+k]中并让 i += 1 ,即指针往后移。否则我们就将 nums[j]放入数组nums[l+k] 中并让 j += 1 。如果有一个指针已经移到了区间的末尾,那么就把另一个区间里的数按顺序加入数组nums中即可。那么整个归并过程结束后 [l,r]即为有序的
注:图1来源于五分钟学算法
-
复杂度分析
来源于leetcode 912
-
实现
- 自顶向下
void _merge(int arr[],int l,int mid,int r)
{
//分配大小为r-l+1的辅助数组
int aux[r-l+1];
for(int i=l;i<=r;i++)
{
//偏移量为l
aux[i-l]=arr[i];
}
int i=l,j=mid+1;
for(int k=l;k<=r;k++)
{
//左边用尽,取右边的元素
if(i>mid)
{
arr[k]=aux[j-l];
j++;
}
//右边用尽,取左边的元素
else if(j>r)
{
arr[k]=aux[i-l];
i++;
}
//右边元素大于左边,取左边
else if(aux[i-l]<aux[j-l])
{
arr[k]=aux[i-l];
i++;
}
//左边元素大于等于右边,取右边
else
{
arr[k]=aux[j-l];
j++;
}
}
}
void _merge_sort(int arr[],int l,int r)
{
if(l>=r)
return;
int mid=l+(r-l)/2;
//归并排序[l,mid]
_merge_sort(arr,l,mid);
//归并排序[mid+1,r]
_merge_sort(arr,mid+1,r);
//归并
_merge(arr,l,mid,r);
}
void merge_sort(int arr[],int n)
{
//范围[l,r]
_merge_sort(arr,0,n-1);
}
int main()
{
int n=10;
int *arr=new int[n];
generateRandomArray(arr,n,10,50);
PrintArray(arr,n);
merge_sort(arr,n);
PrintArray(arr,n);
// test_sort("select_sort",select_sort,arr,n);
}
-
优化:
1._merge之前判断是否有序
- [l,mid],[mid+1,r]两个区间的元素都为有序的,如果nums[mid+1]>nums[mid],那么[l,r]部分已经有序,就可以不用进行归并操作。
2.对小规模子数组使用插入排序
- 插入排序对于近乎有序的数组,时间复杂度为O(n)级别。
void insert_sort_3(int arr[],int l,int r)
{
for(int i=l;i<=r;i++)
{
int e=arr[i];
int j;
for(j=i;j>l;j--)
{
if(arr[j-1]>e)
arr[j]=arr[j-1];
else
break;
}
swap(arr[j],e);
}
}
- 自顶向上
归并:从子序列长度为1(k)开始,进行两两归并,得到2*k的有序序列;
循环:子序列长度为2k开始,进行两两归并,终止条件直到原数组遍历完成。
void merge_sortBU(int arr[],int n)
{
//k:当前子序列的长度
for(int k=1;k<n;k+=k)
for(int low=0;low+k<n;low+=2*k)
_merge(arr,low,low+k-1,min(low+k+k-1,n-1));
}
int main()
{
int n=10;
int *arr=new int[n];
generateRandomArray(arr,n,0,20);
PrintArray(arr,n);
merge_sortBU(arr,n);
PrintArray(arr,n);
// test_sort("select_sort",select_sort,arr,n);
}
-
结果
5. 堆排序
-
原理:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足如下性质:
-
堆中某个节点的值总是不大于或不小于其父节点的值;
-
堆总是一棵完全二叉树。
- 大顶堆:每个结点的值都大于或等于其左右孩子结点的值;
- 小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
对于堆(一颗完全二叉树)的表示, 我们采用顺序结构对数据进行存储,可以根据如下性质得到元素的下标(数组下标从1开始):
- 如果2i小于n,则编号为i的结点的左孩子编号为2i,如果2i大于n,则该结点没有左孩子。
- 如果2i+1小于n,则编号为i的结点的右孩子编号为2i+1,如果2i+1大于n,则该结点没有右孩子。
- 编号为count的结点,其父节点的编号为:count/2;
堆的基本属性:
- data[],数组
- count,当前存放的元素数量,初始化时为0;
- capacity,最大容量
#include<iostream>
#include<ctime>
#include <cstdlib>
#include<cmath>
using namespace std;
class MaxHeap{
public:
MaxHeap(int maxsize)
{
//max size
capacity=maxsize;
data=new int[maxsize+1];
count=0;
}
bool is_empty()
{
return count==0;
}
int size()
{
return count;
}
private:
//存放的数据
int *data;
//当前的size
int count;
//最大容量
int capacity;
};
构建堆(插入新元素)
- 在数组的末尾插上新的元素;
- 数组的size:count++;
- 调整元素,使其满足堆的性质(shift up操作);
比如我们已经有一个最大堆,76,32,8,7,插入的新元素为:40。
比较结点40和其父结点比较,使其满足堆的性质,如果比父结点大,则交换,否则已经满足最大堆了。
void shiftup(int k)
{
while(k>1&&data[k]>data[k/2])
{
swap(data[k],data[k/2]);
k=k/2;
}
}
void insert(int num)
{
data[count+1]=num;
count++;
shiftup(count);
}
取出最大元素
- 取出data[0](堆的最大元素);
- 交换data[0],data[count]
- count减一;
- 调整元素,使其满足堆的性质(shift down操作)
int extractMax()
{
int temp=data[1];
swap(data[1],data[count]);
count--;
shift_down(1);
return temp;
}
void shift_down(int k)
{
while(2*k<=count)
{
int j=2*k;
if(j+1<=count&&data[j+1]>data[j])
j++;
if(data[k]>=data[j])
break;
swap(data[k],data[j]);
k=j;
}
}
查找:
1.顺序查找
-
原理
- 思想:
顺序查找就是在关键字集合中找出与给定的关键字相等的元素。
步骤:
(1)从文件的第一个记录开始,将每个记录的关键字与给定的关键字比较。
(2)如果查找到某个记录的关键字等于 key,则查找成功,返回该记录在文件中的位置;如果所有的记录都进行了比较,仍未找到与 k 相等的记录,则给出 0,表示查找失败。
- 时间复杂度:O(n)
-
实现
#include <iostream> using namespace std; typedef struct student { unsigned int id; char name[10]; int score; }Student; int searchinfo(Student *stu,int num,int id) { for(int i=0;i<num;i++) { if(stu[i].id==id) return i; } return -1; } int main() { Student stu[4]={{1004,"TOM",100}, {1002," LILY",95}, {1001,"ANN",93}, {1003,"luCY",98}}; int address; address=searchinfo(stu,4,1001); std::cout<<"ID: "<<stu[address].id<<std::endl; std::cout<<"name: "<<stu[address].name<<std::endl; std::cout<<"score: "<<stu[address].score<<std::endl; return 0; }
2.折半查找
-
原理:
- 思想:
二分查找的思想是利用分治法,逐渐将查找的数据集范围缩小。
具体过程:
将待查的关键字 k 与当前查找范围内位置居中的关键字比较,如果相等,则查找成功,返回被查到记录在文件中的位置;如果 k 小于当前居中记录的关键字,则对当前查找范围的前半部分重复上述过程,否则到当前查找范围的后半部分重复上述过程。如果查找失败返回NULL。
- 时间复杂度:O(log2n)
-
实现:
- 闭区间:[l,r]
#include<iostream> using namespace std; int Binary_search(int arr[],int n,int target) { int l=0,r=n-1;//在[l,r]闭区间搜索target while(l<=r)//当l==r时,区间依然有效 { int mid=l+(r-l)/2; if(arr[mid]==target) { return mid+1; } else if(arr[mid]<target) { l=mid+1;//在[mid+1,r]闭区间搜索target } else { r=mid-1;//在[l,mid-1]闭区间搜索target } } return -1; } int main() { int arr[5]={0,1,5,6,7}; int index=Binary_search(arr,5,10); cout<<index<<endl; return 0; }
- 半开区间:[l,r)
#include<iostream> using namespace std; int Binary_search(int arr[],int n,int target) { int l=0,r=n;//在[l,r)区间搜索target while(l<r)//当l==r时,区间无效 { int mid=l+(r-l)/2; if(arr[mid]==target) { return mid+1; } else if(arr[mid]<target) { l=mid+1;//在[mid+1,r)区间搜索target } else { r=mid;//在[l,mid-1)区间搜索target } } return -1; } int main() { int arr[5]={0,1,5,6,7}; int index=Binary_search(arr,5,7); cout<<index<<endl; return 0; }
- 注意:
二分查找的区间问题。
3.二叉查找树
-
原理
- 简介:
二叉查找树是一种具有良好排序和查找性能的二叉树数据结构。二叉查找树支持多种基本操作,包括查找、按序遍历、求最大值和最小值、查找前驱结点和后继结点、插入和删除结点等。一般将它用于查找字典或优先队列结构。这些操作在二叉查找树上的平均执行时间是 O( log2 n )。
- 定义:
1. 它是一棵二叉树,如果根结点的左子树不空,则左子树中所有的结点值均小于根结点。
2. 如果根结点的右子树不为空,则右子树中所有的结点值均大于根结点。
3. 二叉查找树的每一棵子树也是一棵二叉查找树。
-
实现
#include<iostream>
using namespace std;
typedef struct node{
int data;
struct node *left;
struct node *right;
}Node;
void create_tree(Node *Tree, int data)
{
//初始化新结点
Node* new_node=new Node;
new_node->data=data;
new_node->left=NULL;
new_node->right=NULL;
Node* cur=Tree;
while(cur)
{
if(cur->data<data)
{
//右节点为空,直接将新节点连接在右节点上
if(cur->right==NULL)
{
cur->right=new_node;
return ;
}
//右节点不为空,更新当前节点为右节点
else
{
cur=cur->right;
}
}
//同理
else
{
if(cur->left==NULL)
{
cur->left=new_node;
return ;
}
else
{
cur=cur->left;
}
}
}
}
void middle_search(Node* Tree)
{
if(!Tree)
return;
middle_search(Tree->left);
cout<<Tree->data<<" ";
middle_search(Tree->right);
}
int main()
{
Node* Tree=new Node;
Tree->left=NULL;
Tree->right=NULL;
Tree->data=10;
create_tree(Tree,12);
create_tree(Tree,9);
create_tree(Tree,50);
create_tree(Tree,20);
middle_search(Tree);
return 0;
}
参考: