基础排序和查找算法
下面的算法可能要用到的简单函数
#define Type int
//交换函数
void Swap(int&a,int&b)
{
int tmp=a;
a=b;
b=tmp;
}
//输入函数
void Input(int n, int *a) {
for(int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
}
//输出函数
void Output(int n, int *a) {
for(int i = 0; i < n; ++i) {
if(i)
printf(" ");
printf("%d", a[i]);
}
puts("");
}
1、冒泡排序(必会)
冒泡排序就是把小的元素往前调或者把大的元素往后调,比较是相邻的两个元素比较,交换也发生在这两个元素之间。
如果两个相邻的元素是相等的,就不需要将其在交换位置,如果相等的两个元素不相邻,通过交换之后两个元素相邻之后,其相对位置也不会改变,所以冒泡排序是一种稳定的排序算法。
基本冒泡排序:
每次循环比较一轮,找到当前循环的最大值,将其放到最后;
void BubblueSort(int* br, int n)
{
assert(br != nullptr);
//因为最后一个元素不用进行排序,所以只需要进行n-2此排序就可以完成所有的排序了
for (int i = 1; i < n;++i )
{
//每次循环都从首位开始比较,将这次排序中的最大值放到后面
for (int j = 0; j < n - i; ++j)
{
if (*(br + j) > *(br + j+1))
{
Swap_Int(&br[j], &br[j+1]);
}
}
}
}
如果在整个排序的过程中,本来的数组已经是有序的,没有发生交换,那么就可以直接跳出循环了:
void BubblueSort(int* br, int n)
{
assert(br != nullptr);
bool flag = flase;
//因为最后一个元素不用进行排序,所以只需要进行n-2此排序就可以完成所有的排序了
for (int i = 1; i < n;++i )
{
//每次循环都从首位开始比较,将这次排序中的最大值放到后面
for (int j = 0; j < n - i; ++j)
{
if (*(br + j) > *(br + j+1))
{
flag = true;
Swap_Int(&br[j], &br[j+1]);
}
}
if(!flag) break;
}
}
利用两个for循环将函数排序:
内循环是为了找出此次循环中的最大或者最小值,将其放到末尾,然后依次寻找次小值等,直到循环到最后一次。例如:
将原数组 Ar[]={12 ,35 ,43 ,56 ,32, 52,15,8}
按照从小到大的顺序进行排列
总之就是外循环进行一次就进行一次比较,有n个数就循环n-1次。第一层循环是为了计数,第二层循环负责比较。
改进之后的冒泡排序:
每一次循环比较两次,先从上往下比较找到最大的,再从下往上比较找到最小的。
void BubbleSort(int *ar,int n)
{
assert(ar != nullptr);
for (int i = 1; i < n;++i )
{
for (int j = 0,k=n-j-1; j < n - i && k > 0 && k>=j ; ++j,--k)
{
if (*(ar + j) > *(ar + j+1))
{
Swap(ar[j], ar[j+1]);
}
if(ar[k]<ar[k-1])
{
Swap(ar[k],ar[k-1]);
}
}
}
}
2. 直接插入排序
**原理:**插入排序是在一个已经有序的小序列的基础上,一次插入一个一个元素。
最开始的时候这个有序的小序列只有一个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的元素的最大值开始比较,如果比他大就直接插入在其后面,否则一直往前找知道找到他该插入的位置。
如果碰到一个和准备插入元素相等的数据,那么把准备插入元素插入在其后面。所以插入排序不会改变相等元素的相对位置,所以插入排序是稳定的。
-
例题:使用插入排序将一个含有n个元素的数据,按照升序排列
void insertionSort(vector<int>& nums) { if (nums.size() == 0) return; int n = nums.size(); for (int i = 1; i < n; ++i) { if (nums[i] < nums[i - 1]) { int j = i - 1; int tmp = nums[i]; while (j >= 0 && nums[j] > tmp) { nums[j + 1] = nums[j]; j--; } nums[j+1] = tmp;//将tmp放到合适的位置 } } }
3. 希尔排序
希尔排序就是加强版的插入排序。无论是冒泡排序还是希尔排序,如果最大值刚好在第一位,要将他挪到正确的位置就需要n-1挪动。也就是说,原数组的一个元素如果距离他正确位置太远的话,则需要与相邻元素交换很多次才能到大正确的位置,这样相对来说比较耗费时间。
**希尔排序的思想:**先让数组中任意间隔为h的元素有序,刚开始h的大小可以是h=n/2,接着h=n/4,让h一直缩小,当h= 1时,也就是此时数组中任意间隔为1的元素有序,此时数组就是有序的了。
步骤:
- 先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…
- 当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。
//希尔排序
void shellSort(vector<int>& nums)
{
int n = nums.size();
int gap = n/2;//分组大小
//每次循环使用插入排序的方式,将小分组里面的数据有序
for (; gap > 0; gap /= 2)
{
for (int i = gap; i < n; ++i)
{
int j = i - gap;
int tmp = nums[i];//将插入值提前保存下来,以免被覆盖
for (; j >= 0 && nums[j] > nums[i]; j -= gap)
{
nums[j + gap] = nums[j];
}
nums[j + gap] = tmp;
}
for (auto a : nums)
{
cout << a <<" ";
}
cout << endl;
}
}
其实本质上希尔排序就是,在插入排序之前先进性预排序,是的序列基本有序之后,在进行一次插入排序就达到效果。
其核心代码:
for (int i = gap; i < n; ++i)
{
int j = i - gap;
//将插入值提前保存下来,以免被覆盖
int tmp = nums[i];
for (; j >= 0 && nums[j] > nums[i]; j -= gap)
{
nums[j + gap] = nums[j];
}
nums[j + gap] = tmp;
}
实际上就是将插入排序的步长从1改为了gap
4. 选择排序
选择排序就是每次选出待排序队列中的中的最小值,然后将其放到已排序队列的最后
void selectSort(vector<int>& nums)
{
int n = nums.size();
for(int i = 0;i < n; ++i)
{
int j = i;
int min = j;
for(;j < n; ++j)
{
min = nums[j] < nums[min] ? j : min;
}
swap(&nums[i],&nums[min]);
}
}
因为每个数都要循环比较n次,所以时间复杂度是指数级别的。
5. 归并排序(必会)
**思想:**将一个大的无序数组有序,可以将这个大的数组分成两个,然后对这两个数组分别进行排序,之后再把这两个数组合并成一个有序数组。由于两个数组都是有序的,所以合并是很快的。
通过递归的方式,直到数组的大小为1,此时只有一个元素,那么该数组就是有序的了,之后再把两个数组大小合并为一个大小为2的,在依次合并直到整个数组都有序。
归并排序是建立在归并操作上的一种有效的排序算法。该算法采用的是分治法,
步骤:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
void mergearray(int a[],int first,int mid,int last,int temp[]) //将两个有序数组合并排序
{
int i=first,j=mid+1;
int m=mid,n=last;
int k=0;
while(i<=m&&j<=n)
{
if(a[i]<a[j])
temp[k++]=a[i++];
else
temp[k++]=a[j++];
}
while(i<=m)
temp[k++]=a[i++];
while(j<=n)
temp[k++]=a[j++];
for(i=0;i<k;i++)
a[first+i]=temp[i];
}
void mergesort(int a[],int first,int last,int temp[]) //将两个任意数组合并排序
{
if(first<last)
{
int mid=(first+last)/2+first;
mergesort(a,first,mid,temp); //左边有序
mergesort(a,mid+1,last,temp); //右边有序
mergearray(a,first,mid,last,temp); //再将两个有序数组合并
}
}
bool MergeSort(int a[], int n)
{
int *p = new int[n]; //分配一个有n个int型元素的数组所占空间,并将该数组的第一个元素的地址赋给int *型指针p。
if (p == NULL)
return false;
mergesort(a, 0, n - 1, p);
delete[] p;
return true;
}
或者另一种写法:
void mergeSortCore(vector<int>& nums, vector<int>& copy, int begin, int end) {
if (begin >= end) return;
int mid = begin + (end - begin) / 2;
int low1 = begin, high1 = mid, low2 = mid + 1, high2 = end;
//这里减少了copy向nums的赋值部分,千万注意不要把copy 和 nums赋值反了
mergeSortCore(copy, nums, low1, high1);
mergeSortCore(copy, nums, low2, high2);
int copyIndex = low1;
while (low1 <= high1 && low2 <= high2)
{
copy[copyIndex++] = nums[low1] < nums[low2] ? nums[low1++] : nums[low2++];
}
while (low1 <= high1)
{
copy[copyIndex++] = nums[low1++];
}
while (low2 <= high2)
{
copy[copyIndex++] = nums[low2++];
}
cout << begin << " " << end << endl;
}
void mergeSort(vector<int> nums)
{
vector<int> copyNums(nums);//这里要借助一个一模一样的数组的
mergeSortCore(nums, copyNums, 0, nums.size() - 1);
nums.assign(copyNums.begin(), copyNums.end());//到最后copy数组是排序好的,记得要赋值一下
}
归并排序注重的是分治思想,先将大数组分成一个个的小数组,在从小到大依次合并,合并两个有序数组的时间复杂度可以降低到O(n),分解的时间复杂度为log(n),那么最终的时间复杂度应该为,O(n*logn)。
6. 快速排序(必会)
快速排序也是考察率较高的一种排序算法,主要操作步骤是每次选取一个划分元,将比这个划分元大的数都放到其右边,比划分元小的数都放到其左边,直到划分区间变为一停止
//主要是依靠划分函数来进行排序
int Partation(vector<int>& nums, int start,int end)
{
if(start >= end) return;
int tmp = nums[start];//选取划分元
int left = start , right = end;
while(left < right)
{
while(left < right && nums[right] >= tmp)
{
right--;
}
if(left < right && nums[right] < tmp)
{
nums[left] = nums[right];
left++;
}
while(left < right && nums[left] <= tmp)
{
left++;
}
if(left < right && nums[left] > tmp)
{
nums[right] = nums[left];
right--;
}
}
nums[left] = tmp;
return left;
}
void quickSortFun(vector<int>& nums,int start,int end)
{
if(start < end)
{
int mid = Partation(nums,start,end);
quickSortFun(nums,start,mid);
quickSortFun(nums,mid + 1,end);
}
}
void quickSort(vector<int>& nums)
{
int n = nums.size();
if(n <= 1) return;
quickSortFun(nums,0,nums.size()-1);
}
当快速排序遇到极特殊情况时,如有序数组,那么快速排序的时间复杂度将快速增大,最终退化成冒泡排序时间复杂度为O(n^2),那么应该如何改进才能使得快速排序在任何情况下的时间复杂度都为 O(nlog(n)) 呢?
关键点在于划分元的选择上,详细描述在我的另一篇文章中有所描述:
非递归版本
//单趟排
int PartSort(int* arr, int begin, int end)
{
int key = arr[begin];
while (begin < end)
{
while (key <= arr[end] && begin < end)
{
--end;
}
arr[begin] = arr[end];
while (key >= arr[begin] && begin < end)
{
++begin;
}
arr[end] = arr[begin];
}
arr[begin] = key;
int meeti = begin;
return meeti;
}
void QuickSortNoR(int* arr, int begin, int end)
{
stack<int> st;
//先入右边
st.push(end);
//再入左边
st.push(begin);
while (!st.empty())
{
//左区间
int left = st.top();
st.pop();
//右区间
int right = st.top();
st.pop();
//中间数
int mid = PartSort(arr, left, right);
//当左区间>=mid-1则证明左区间已经排好序了
if (left < mid - 1)
{
st.push(mid - 1);
st.push(left);
}
//当mid+1>=右区间则证明右区间已经排好序
if (right > mid + 1)
{
st.push(right);
st.push(mid + 1);
}
}
}
7. 堆排序
利用堆这种数据结构完成的排序算法,,堆排序算法是一种选择排序算法,他的最好,最坏时间复杂度都是O(nlogn),是不稳定的排序算法。
对于堆排序难点在于,二叉树的顺序数组储存到大顶堆(小顶堆)的转换。从数据存储来看,数组存储方式和树的存储方式可以相互转换,既数组可以转换成树,树也可以转换成数组。
堆是具有以下特点的完全二叉树:每个节点都大于会等于其左右孩子节点的值,称为大顶堆;每个节点的值都小于或等于左右孩子的值,称为小顶堆。
代码实现思路:
-
构造初始堆:将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
-
构建初始堆时,要注意,完全二叉树的第一个非叶子节点的下标是n/2
//大顶堆实现无序数组的升序排列 void heapSort(vector<int>& nums) { int n = nums.size(); //此循环为构建初始堆 for(int i = n/2;i >= 0; --i) { int j = 2*i+1; //判断有没有越界 while(j+1 < n) { //比较知道根节点左右子树中的较大值 if(nums[j] < nums[j+1]) { j++; } if(nums[i] < nums[j]) { Swap(&nums[i],&nums[j]); i = j; } else { break; } } } for(int i = n-1;i>0;--i) { //将数组的最大值也就是大顶堆的顶点,放到数组的最后面 Swap(&nums[i],&nums[0]); //对剩下的n-1个值继续构建大顶堆,寻找次大值,依次循环,直到所有的数据排序完成 int k = 0; while((2*k+1) < i) { int j = 2*k+1; if(nums[j] < nums[j+1]) { j++; } if(nums[k] < nums[j]) { Swap(&nums[k],&nums[j]); //因为发生了交换,不知道有没有影响到子树成堆,所以需要继续比较 k = j; } else { break; } } } }
void heapSort(vector<int>& nums) { int n = nums.size(); //构建初始堆,和大顶堆正好相反 for (int i = n / 2; i >= 0; --i) { int j = 2 * i + 1; while((j + 1) < n) { if (nums[j] > nums[j + 1]) { j++; } if (nums[i] > nums[j]) { Swap(nums[i], nums[j]); i = j; } else { break; } } } for (int i = n - 1; i > 0; --i) { //将最小值挪到最后 Swap(nums[i], nums[0]); int k = 0; while((2 * k + 1) < i) { int j = 2 * k + 1; if ((j + 1) < i) { if (nums[j] > nums[j + 1]) { j++; } } if (nums[k] > nums[j]) { Swap(nums[k], nums[j]); k = j; } else { break; } } } }
-
什么是堆?
堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。
堆的特性:
• 堆中某个节点的值总是不大于或不小于其父节点的值
• 堆总是一棵完全二叉树
大顶堆和小顶堆
大顶堆:所有的父节点大于等于孩子节点
小顶堆:所有的父节点小于等于孩子节点
堆排序的基本步骤
-
首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
-
将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
-
将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
8.计数排序
计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。
它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当 的时候其效率反而不如基于比较的排序,因为基于比较的排序的时间复杂度在理论上的下限是 。
算法思路:
计数排序对输入的数据有附加的限制条件:
- 输入的线性表的元素属于有限偏序集 S;
- 设输入的线性表的长度为 n,|S|=k(表示集合 S 中元素的总数目为 k),则 k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
计数排序的基本思想是对于给定的输入序列中的每一个元素 x,确定该序列中值小于 x 的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将 x 直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有 17 个元素的值小于 x 的值,则 x 可以直接存放在输出序列的第 18 个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
算法过程:
- 根据待排序集合中最大元素和最小元素的差值范围,申请额外空间;
- 遍历待排序集合,将每一个元素出现的次数记录到元素值对应的额外空间内;
- 对额外空间内数据进行计算,得出每一个元素的正确位置;
- 将待排序集合每一个元素移动到计算得出的正确位置上。
代码实现
void countingSort(vector<int>& nums)
{
int n = nums.size();
if (n <= 1) return;
int max = 0, min = 0;
//遍历数组找到最大值和最小值
for (int i = 0; i < n ; ++i)
{
max = nums[i] > max ? nums[i] : max;
//min = nums[i] < min ? nums[i] : min;
}
//创建一个额外数组,用来计数
vector<int> tmp(max+1);
for (int i = 0; i < n; ++i)
{
tmp[nums[i]]++;
}
for (int i = 0,j = 0; i < (max + 1); ++i)
{
while (tmp[i]--)
{
nums[j++] = i;
}
}
}
但是:
虽然计数排序看上去很强大,但是它存在两大局限性:
- 当数列最大最小值差距过大时,并不适用于计数排序
比如给定 20 个随机整数,范围在 0 到 1 亿之间,此时如果使用计数排序的话,就需要创建长度为 1 亿的数组,不但严重浪费了空间,而且时间复杂度也随之升高。
- 当数列元素不是整数时,并不适用于计数排序
如果数列中的元素都是小数,比如 3.1415,或是 0.00000001 这样子,则无法创建对应的统计数组,这样显然无法进行计数排序。
9. 桶排序
将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。
算法思想:
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
这里值得我们注意的是,桶的数量要取到一个适中值是比较困难的,通的数量过多或者过少都会导致桶排序退化,过多回退化成计数排序,太少回退化成比较排序。但是有没有一个特定的公式来确定桶的数量.所以我们还是只能自己确定桶的数量.但是有一个规则我们还是可以考虑进去的,那就是最好让元素平均的分散到每一个桶里
.
function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}
var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; // 输入数据的最大值
}
}
// 桶的初始化
var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
// 利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}
arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
10.基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
1)算法描述
取得数组中的最大数,并取得位数
arr为原始数组,从最低位开始取每个位组成radix数组
对radix进行计数排序(利用计数排序适用于小范围数的特点)
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
var counter = [];
function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for(var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if(counter[bucket]==null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for(var j = 0; j < counter.length; j++) {
var value = null;
if(counter[j]!=null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
排序算法没有优劣,在适当的情况下使用相应的方法
二分查找
**原理:**二分查找的前提是有序数组,通过比较中位数和目标数据的大小,来定位下次寻找的区间。
给定一个有序数组nums,在数组中寻找目标值target,如果找到了返回目标值的下标,如果没找到目标值返回-1;
int binaryLookUp(vector<int> nums,int target)
{
if(nums.size() <= 0) return -1;
int left = 0,right = nums.size()-1;
while(left <= right)
{
int mid = (right - left)/2 + left;
if(nums[mid] < target)
{
right = mid - 1;
continue;
}
else if(nums[mid] > target)
{
left = mid;
continue;
}
eles
{
return mid;
}
}
if(left > right) return -1;
}
循环移动数组
循环移动数组;
示例:
int ar[10]={1, 2 , 3 , 4 , 5 , 6 ,7 , 8 , 9 , 10};右移一个数据元素:
输出{ 10,1,2,3,4,5,6,7,8,9};
右移k个数据元素:
如k = 3;输出{8,9,10,1,2,3,4,5,6,7};
实现函数: Right_Move_Array; // 右移一个数据元素Right_Move_Array_K; // 右移k 个数据元素Left_Move_Array;
Left_Move_Array_K
循环移动可以
void Swap_Int(int* ap, int* bp)
{
assert(ap != nullptr&&bp!=nullptr);
int tmp = 0;
tmp = *ap;
*bp = *ap;
*ap = tmp;
}
//让br[]数组的所有元素依次向左移动一位
void Left_Move_Array(int* br, int n) {
assert(br != nullptr);//断言很重要,在程序运行的时候如果出现错误可以及时停止程序,减少时间的浪费
int tmp = br[0];
br[0] = 0;
for (int i = 1; i < n; ++i)
{
br[i-1]=br[i];//将第一个数取出之后,后面的数字依次向前挪一个位置然后让第一个数赋给最后一个位置
}
br[n - 1] = tmp;
}
void right_Array_move(int* br, int n)
{
assert(br != nullptr);
int tmp = br[n-1];
br[n-1] = 0;
for (int i = n - 1; i > 0; --i)
{
br[i] = br[i - 1];
}
br[0] = tmp;
}
void left_move_array_k(int* br, int n, int k)
{
assert(br != nullptr);
k = k % n;
for (int i = 0; i < k; ++i)
{
Left_Move_Array(br, n);
}
/*int tmp = br[n - k - 1];
br[n - 1 - k] = 0;
for (int i = 0; i < n - 1; ++i)
{
br[i] = br[(i + k)%n];
}
br[n - 1 - k] = tmp;*/
}
void right_move_array_k(int* br, int n, int k)
{
assert(br != nullptr);
left_move_array_k(br, n, -k);
/*k = k % n;
while (k--)
{
right_Array_move(br, n);
}*/
}