1 插入排序
(1)直接插入排序
//直接插入排序
void insertSort(int a[], int n) {
for (int i = 1; i < n; ++i) {
int temp = a[i];//待插入元素
int j;
for (j = i; j >= 1 && temp < a[j-1]; --j) {//0-i-1有序
a[j] = a[j-1];
}
a[j] = temp;
}
}
基本操作是元素移动次数,时间复杂度O(n2)
(2)折半插入排序
void binInsertSort(int a[], int n) {
for (int i = 1; i < n; i++) {
int temp = a[i];
int l = -1;
int r = i;
while (r - l > 1) {//找大于等于temp的最小元素(r)
int mid = l + (r - l) / 2;
if (a[mid] >= temp) {
r = mid;
}
else {
l = mid;
}
}
int j = i - 1;
for (; j >= r; --j) {//r到i-1的元素整体后移一位
a[j + 1] = a[j];
}
a[j + 1] = temp;
//a[r]=temp;
}
}
基本操作次数没有改变,因此时间复杂度依然为O(n2)
(3)希尔排序
void shellSort(int a[], int n) {
for (int gap = n / 2; gap >= 1; gap /= 2) {
for (int i = gap; i < n; ++i) {
int temp = a[i];
int j = i;
for (; j >= gap && temp < a[j - gap]; j -= gap) {
a[j] = a[j - gap];
}
a[j] = temp;
/* int j = i - gap;
for (; j >= 0 && temp < a[j]; j -= gap) {
a[j + gap] = a[j];
}
a[j + gap] = temp;*/
}
}
}
时间复杂度与增量的选取有关,以上采用的选取方法时间复杂度为O(n2)
交换类排序
(1)冒泡排序
void bubbleSort(int a[], int n) {
for (int i = 0; i < n - 1; ++i) {
bool flag = true;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = false;
}
}
if(flag){//若在一趟冒泡中未交换说明已经有序
break;
}
}
}
时间复杂度O(n2)
(2)快速排序
快速排序1.0
左边都是小于等于,右边都是大于。(默认以第一个元素做划分)
void quickSort(int a[], int low, int high) {
if (low < high) {
int temp = a[low];
int r = low;//先剔除第一个元素,所以初始小于等于的右边界为low
int i = low + 1;
while (i <= high) {
if (a[i] <= temp) {//如果小于等于temp,则与右边界的下一个数作交换,同时右边界右移
swap(a[i], a[r + 1]);
++r;
++i;
}
else {
++i;
}
}
swap(a[r], a[low]);//最后右边界上数与第一个元素交换,这样r左边都是小于等于的,右边都是大于的
quickSort(a, low, r - 1);
quickSort(a, r + 1, high);
}
}
快速排序2.0
左边小于,中间等于,右边大于
void quickSort(int a[], int low, int high) {
if (low < high) {
int temp = a[low];
int r = low;//小于的右边界
int l = high + 1;//大于的左边界
int i = low + 1;//先剔除第一个元素
while (i < l) {
if (a[i] < temp) {//若小于temp,则与右边界的下一个值交换,同时右边界向右扩充
swap(a[i], a[r + 1]);
++i;
++r;
}
else if (a[i] > temp) {//若大于temp,则与左边界的下一个值交换,同时左边界向左扩充
swap(a[i], a[l - 1]);
--l;
}
else {
++i;
}
}
swap(a[low], a[r]);//最后将右边界上的数与第一个元素交换,同时右边界左移一位
--r;
quickSort(a, low, r);//此时r是小于的右边界,l是大于的左边界,(r,l)之间就是相等的元素。
quickSort(a, l, high);
}
}
快排3.0
随机选择一个数作划分。
void quickSort(int a[], int low, int high) {
if (low < high) {
int n = high - low + 1;
int index = rand() % n;//随机选择一个位置
swap(a[low], a[low + index]);//先将划分值交换到第一个位置
int r = low;
int l = high + 1;
int i = low + 1;
while (i<l) {
if (a[i] < a[low]) {
swap(a[i], a[r + 1]);
++r;
++i;
}
else if (a[i] > a[low]) {
swap(a[i], a[l - 1]);
--l;
}
else {
++i;
}
}
swap(a[r], a[low]);
quickSort(a, low, r - 1);
quickSort(a, l, high);
}
}
另一种写法
void quickSort(int a[], int low, int high) {
if (low < high) {
int temp = a[low];
int i = low;
int j = high;
while (i < j) {
while (i < j && a[j] >= temp)--j;
if (i < j) {
a[i++] = a[j];
}
while (i < j && a[i] < temp)++i;
if (i < j) {
a[j--] = a[i];
}
}
a[i] = temp;
quickSort(a, low, i - 1);
quickSort(a, i + 1, high);
}
}
时间复杂度O(NlogN),空间复杂度O(logN)
应用:
找第k大的元素:每一轮快排都选择一个枢轴,使得左边的元素都小于这个枢轴,右边的元素都大于等于这个枢轴。
若枢轴右边的元素(包含枢轴)正好等于k的话,说明枢轴就是第k大的元素;
若枢轴右边的元素(包含枢轴)小于k的话,说明第k大的元素在枢轴左边;
若枢轴右边的元素(包含枢轴)大于k的话,说明第k大的元素在枢轴右边.
int kMostLarge(int a[], int n,int low, int high, int k) {//第k大的元素
if (low <= high) {
int temp = a[low];
int i = low;
int j = high;
while (i < j) {
while (i < j && a[j] >= temp)--j;
if (i < j) {
a[i++] = a[j];
}
while (i < j && a[i] < temp)++i;
if (i < j) {
a[j--] = a[i];
}
}
a[i] = temp;
if (n - i == k) {//i右边的元素个数(包含i)
return a[i];
}
else if (n - i < k) {
return kMostLarge(a, n, low, i - 1, k);
}
else {
return kMostLarge(a, n, i + 1, high, k);
}
}
return -1;
}
选择排序
(1)简单选择排序
void selectSort(int a[], int n) {
for (int i = 0; i < n - 1; ++i) {
int minPos = i;
for (int j = i + 1; j < n; ++j) {
if (a[j] < a[minPos]) {
minPos = j;
}
}
int temp = a[minPos];
a[minPos] = a[i];
a[i] = temp;
}
}
(2)堆排序
void sift(int a[], int low, int high) {//调整函数
int i = low;
int j = 2 * i + 1;
int temp = a[i];
while (j <= high) {
if (j<high && a[j + 1]>a[j])++j;
if (a[j] > temp) {
a[i] = a[j];
i = j;
j = 2 * j + 1;
}
else {
break;
}
}
a[i] = temp;
}
void heapSort(int a[], int n) {
for (int i = n / 2 - 1; i >= 0; --i) {//初始堆
sift(a, i, n-1);
}
for (int i = n - 1; i >= 1; --i) {
int temp = a[i];
a[i] = a[0];
a[0] = temp;
sift(a, 0, i - 1);
}
}
二路归并排序
void merge(int a[], int low, int mid, int high) {
int n1 = mid - low + 1;
int n2 = high - mid;
int* a1 = new int[n1];
int* a2 = new int[n2];
for (int i = 0; i < n1; ++i) {
a1[i] = a[low + i];
}
for (int i = 0; i < n2; ++i) {
a2[i] = a[mid +1+ i];
}
int i = 0;
int j = 0;
int k = 0;
while (i < n1 && j < n2) {
if (a1[i] <= a2[j]) {
a[low+k] = a1[i++];
k++;
}
else {
a[low + k] = a2[j++];
k++;
}
}
while (i < n1) {
a[low + k] = a1[i++];
k++;
}
while (j < n2) {
a[low + k] = a2[j++];
k++;
}
}
void mergeSort(int a[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(a, low, mid);
mergeSort(a, mid + 1, high);
merge(a, low, mid, high);//合并
}
}
非递归实现
void mergeSort2(int a[], int low, int high) {
int n = high - low + 1;//数组的长度
int step = 1;
while (step < n) {
int ls = low;//左边的子数组的开始位置
while (ls < n) {
int lf = ls + step - 1;//左边的子数组的结束位置
if (lf >= n) {
break;
}
int rf = min(lf + step, n - 1);//右边子数组的结束位置
merge(a, ls, lf, rf);
ls = rf + 1;
}
if (step > n / 2) {//防止step溢出(超出int最大值)
break;
}
step = step << 1;
}
}
归并排序的时间复杂度O(N*lonN),空间复杂度O(N),要借助辅助数组存放左右数组
应用:
1.规定一个整型数组,计算每个数左边比它小的数之和,最后返回所有数的这些和之和。
例如(2,4,3,4),2左边没有比他小的,4左边有一个2比他小,3左边有一个2比他小,4左边有2和3比它小。所有最终返回2+2+2+3=9.
分析:转换思路,求每个数左边比它小的数转换为这个数右边有几个数比它大。
采用归并策略:假设当前有左右两个数组,两个指针分别指向这两个数组的起始位置。
如果左边指针所指的元素比右边的小,则在右边数组这个范围内从右边数组指针所指元素开始到右边数组的结束位置都比左边大。然后将左边拷贝到原数组,左指针后移;
如果左边指针所指的元素比右边的大,则右边没有比它大的,右边拷贝到原数组,右指针后移;
如果左边指针所指的元素等于右边的,则右边拷贝到原数组,右指针后移。(因为无法确定比左边大的个数)
int merge(int a[], int low, int mid, int high) {
if (low <= high) {
int n1 = mid - low + 1;
int n2 = high - mid;
int* a1 = new int[n1];
int* a2 = new int[n2];
for (int i = 0; i < n1; ++i) {
a1[i] = a[low + i];
}
for (int i = 0; i < n2; ++i) {
a2[i] = a[mid + 1 + i];
}
int i = 0;
int j = 0;
int k = 0;
int re = 0;
while (i < n1 && j < n2) {
if (a1[i] < a2[j]) {
a[low + k] = a1[i];
re += (n2 - j) * a1[i];
k++;
i++;
}
else {
a[low + k] = a2[j++];
k++;
}
}
while (i < n1) {
a[low + k] = a1[i++];
k++;
}
while (j < n2) {
a[low + k] = a2[j++];
k++;
}
return re;
}
}
int lessSum(int a[], int low, int high) {//返回low到high范围上左边比它小的数之和
if (low == high) {
return 0;
}
if (low < high) {
int mid = (low + high) / 2;
return lessSum(a, low, mid) +
lessSum(a, mid + 1, high) +
merge(a, low, mid, high);
}
}
- 给定一个整型数组a,对于a中的某一个元素num,如果num右边有个数的2倍都比num小,则算一个。求所有数的和之和。
例如:{1,3,1,4}
1右边没有
3右边有1个(1)
1右边没有
4右边没有
分析:假设有两个已排好序的左右数组,两个指针分别指向数组的开始位置
如果左指针的元素>右指针的元素*2,右指针后移;
否则当前左指针所指元素的和为右指针开始到当前右指针的元素个数
int merge(int a[], int low, int mid, int high) {
int n1 = mid - low + 1;
int n2 = high - mid;
int* a1 = new int[n1];
int* a2 = new int[n2];
for (int i = 0; i < n1; ++i) {
a1[i] = a[low + i];
}
for (int i = 0; i < n2; ++i) {
a2[i] = a[mid + 1 + i];
}
int i = 0;
int j = 0;
int k = 0;
int re = 0;
int rWindow = mid + 1;//[mid+1,rWindow)为右数组已判断的范围
while (i<n1) {
while (rWindow<=high && a1[i]>a[rWindow] * 2) {
rWindow++;
}
re += (rWindow - mid - 1);
++i;
}
i = 0;
j = 0;
while (i < n1 && j < n2) {
if (a1[i] < a2[j]) {
a[low + k] = a1[i];
k++;
i++;
}
else {
a[low + k] = a2[j++];
k++;
}
}
while (i < n1) {
a[low + k] = a1[i++];
k++;
}
while (j < n2) {
a[low + k] = a2[j++];
k++;
}
return re;
}
int twoSum(int a[], int low, int high) {
if (low == high) { return 0; }
if (low < high) {
int mid = (low + high) / 2;
return twoSum(a, low, mid) + twoSum(a, mid + 1, high) + merge(a, low, mid, high);
}
}
- 子数组个数
分析:本题是求子数组之和落在固定范围内的子数组的个数
首先准备一个前缀和数组sum,sum[i]表示从0到i位置的元素之和。
于是可以转化为求以i结尾的子数组且之和落在固定范围内的个数(i从0到n-1)。
如果sum[i]=Y,即求sum[j]>=Y-upper && sum[j]<=Y-lower的个数。
因此以i结尾的子数组且之和落在固定范围内的个数转化为sum[j]>=Y-upper && sum[j]<=Y-lower的个数。
#define ll long long
class Solution {
public:
int merge(vector<ll>&sum,int low,int mid,int high,int lower,int upper){
int n1=mid-low+1;
int n2=high-mid;
int* a1=new int[n1];
int* a2=new int[n2];
for(int i=0;i<n1;++i){
a1[i]=sum[low+i];
}
for(int i=0;i<n2;++i){
a2[i]=sum[mid+1+i];
}
//[lWindow,rWindow)
int lWindow=low;
int rWindow=low;
int re=0;
for(int i=mid+1;i<=high;++i){
ll min=sum[i]-upper;
ll max=sum[i]-lower;
while(rWindow<=mid && sum[rWindow]<=max)++rWindow;
while(lWindow<=mid && sum[lWindow]<min)++lWindow;
re+=rWindow-lWindow;
}
int i=0;
int j=0;
int k=0;
while(i<n1 && j<n2){
if(a1[i]<=a2[j]){
sum[low+k]=a1[i++];
++k;
}else{
sum[low+k]=a2[j++];
++k;
}
}
while(i<n1){
sum[low+k]=a1[i++];
++k;
}
while(j<n2){
sum[low+k]=a2[j++];
++k;
}
delete []a1;
delete []a2;
return re;
}
int returnCount(vector<ll>&sum,int low,int high,int lower,int upper){
if(low==high){
if(sum[low]>=lower && sum[low]<=upper){
return 1;
}else{
return 0;
}
}
int mid=low+((high-low)>>2);
int leftPart=returnCount(sum,low,mid,lower,upper);
int rightPart=returnCount(sum,mid+1,high,lower,upper);
return leftPart+rightPart+merge(sum,low,mid,high,lower,upper);
}
int countRangeSum(vector<int>& nums, int lower, int upper) {
int n=nums.size();
int result=0;
vector<ll>sum(n);
sum[0]=nums[0];
for(int i=1;i<n;++i){
sum[i]=sum[i-1]+nums[i];
}
return returnCount(sum,0,n-1,lower,upper);
}
};
计数排序
计数排序是一个非基于比较的排序算法,元素从未排序状态变为已排序状态的过程,是由额外空间的辅助和元素本身的值决定的。前提是数据范围有限。
流程:1.为每个数据建一个桶,遍历所有的数据,统计所有数据出现的次数。
2. 按桶的大小依次放值
基于比较的排序极限O(nlogn)
不基于比较的排序极限O(n)
void countSort(vector<int>&nums,int range) {//数据范围1-range
vector<int>bucket (range + 1);
for (int i = 0; i < nums.size(); ++i) {
bucket[nums[i]]++;//统计数据出现的个数
}
int k = 0;
for (int i = 1; i <=range; ++i) {
for (int j = 0; j < bucket[i]; ++j) {
nums[k++] = i;
}
}
}
时间复杂度O(N)
基数排序
基数排序也是不基于比较的排序
流程:假设最大的数有3位,则需要3轮排序。
首先按个位排:
准备10个桶,0-9,个位为0的放0号桶,个位为1的放1号桶……
最后从左往右收集桶中的数(桶中的数按先进先出的顺序收集)
然后再按十位排,重复上述步骤,最后再按百位排。
上面分配-收集的辅助变量可以用队列来完成,但还有一个优化的做法:
举个例子:arr:101,130,234,211,342,240
对上述数按个位排序
首先准备一个count数组,大小为10,下标0位置的值表示个位为0的数有几个,下标1位置表示个位为1的数有几个……
count:
然后再准备一个前缀和数组count’,下标0位置表示个位<=0的数有几个,下标1位置表示个位<=1的数有几个……
最后从右往左arr数组,首先是240,个位为0,查前缀和数组下标0位置是2,那么240一定在1位置上,同时count’0位置减1;
解释一下为什么240一定在1位置上?
count’[0]=2:表示个位小于等于0的数有2个,即个位小于等于0的数一定分布在下标0-1范围上,而又是从右往左收集的,240是最后一个数,必然是占据1这个位置的,否则范围就不会到1.
由于前缀和数组的特性,所以从右往左收集比较方便,如果硬从左往右,需要知道>=0的数有i个,该数一定占据范围的最小位置即n-i。
然后是342,个位为2,查前缀和数组下标2是5,说明个位小于等于2的有5个,则个位小于等于2的收集结果分布在0-4范围,又因为342是最后一个数,则342在4号位置,同时count’2位置减1;
接着是211,最终在3位置
234在5位置
130在0位置
101在2位置
最终收集结果:130,240,101,211,342,234与一般方法得到的结果一致。
int returnDigit(int a[], int n) {//返回数组最大的数有几位
int max = a[0];
for (int i = 1; i < n; ++i) {
if (a[i] > max) {
max = a[i];
}
}
int digits = 0;
while (max != 0) {
++digits;
max /= 10;
}
return digits;
}
int getDigit(int a, int i) {//返回a中第i位的数,如果a不够i位,默认填0
while (--i) {
a /= 10;
}
return a % 10;
}
void precess(int a[], int l, int r, int digits) {
for (int i = 1; i <= digits; ++i) {//一共需要3轮排序,i=1表示按个位,i=2表示按十位……
vector<int>count(10);
for (int j = l; j <= r; ++j) {//求count数组
int k = getDigit(a[j], i);//返回a[j]的第i位
count[k]++;
}
vector<int>count_(10);
count_[0] = count[0];
for (int p = 1; p < 10; ++p) {//求前缀和数组
count_[p] = count_[p - 1] + count[p];
}
vector<int>help(r-l + 1);
for (int q = r; q >= l; --q) {//模拟收集
int k = getDigit(a[q], i);
help[count_[k]-1] = a[q];
count_[k]--;
}
int k = 0;
for (int m = l; m <= r; ++m) {
a[m] = help[k++];
}
}
}
//基数排序
void cardinalitySort(int a[],int n) {
int digits = returnDigit(a,n);//a中最多有几位
precess(a, 0, n - 1, digits);
}
时间复杂度O(n*log10 max),其中max为数组中最大数,log10 max表示最大数的位数,因此基数排序的时间复杂度与最大数的位数有关。
排序的稳定性
定义:排序前相同值的次序与排序后的次序是否一致,若一致则具备稳定性。
不具备稳定性的排序:快速排序,希尔排序,简单选择排序,堆排序