算法学习02:认识O(logN)的排序

01归并排序

归并排序就是先将一个数组的左侧与右侧都有序,然后用两个指针分别比较两个数组元素的大小,将比较结果复制到辅助数组中。
整体采用递归方法,确定递归基是需要排序的序列只有一个数的时候。其余情况,则需要不断向下继续递归。

void mergesort(int l,int r,int arr[])
{
	if(l == r)
		return;
	int m = l+(r-l)>>1;
	mergesort(l,m,arr);
	mergesort(m+1,r,arr);
	merge(l,m,r,arr);
}

如果l=r的时候,说明需要排序的序列只有一个数,必然是有序的,所以直接返回。
接下来看merge方法,将两个有序序列合并成为一个有序序列的过程。

void merge(int l,int m,int r,int arr[])
{
	int help[ARR_MAX];
	int i = 0;
	int p1 = l;
	int p2 = m + 1;
	while(p1<=m &&p2<=r)
	{
		help[i++] = (arr[p1]>arr[p2]? arr[p2++]:arr[p1++]);
	}
	while(p1<=m)
	{
		help[i++] = arr[p1++];
	}
	while(p2<=r)
	{
		help[i++] = arr[p2++] ;
	}
	for(i = 0; i < r-l+1; i++){
		arr[l+i] = help[i];
	}
}

  1. 开辟一个辅助数组,大小与原数组相同,该数组的元素用一个指针位置i来表示。
    用p1代表左侧序列的下标位置,初始值就是左边界l,最大值是m,
    p2代表右侧序列的下标位置,初始值就是m+1,最大值是r。
  2. 若两个序列的指针都没有超过最大值
    若左侧序列的对应元素小于等于右侧序列的对应元素,就将左侧序列的对应元素复制到辅助数组中。若左侧对应数组大于右侧序列的对应元素,则将右侧序列的对应元素复制到辅助数组中。这一过程中,复制元素的序列和辅助数组,对应的指针位置都需要后移。
  3. 若有一个序列的指针越界,则需将另一个序列中剩余的元素都依次复制到辅助数组中。
  4. 最终将辅助数组中的元素,复制到原数组中。

完整测试代码

#include <iostream>
#include<time.h>
#define B_MAX 7
using namespace std;

void merge(int l, int m, int r, int* b)
{
	int help[B_MAX];
	int i = 0;
	int p1 = l;
	int p2 = m + 1;
	while (p1 <= m && p2 <= r)
	{
		help[i++] = (b[p1] > b[p2] ? b[p2++] : b[p1++]);
	}
	while (p1 <= m)
	{
		help[i++] = b[p1++];
	}
	while (p2 <= r)
	{
		help[i++] = b[p2++];
	}
	for (i = 0; i < r - l + 1; i++) {
		b[l + i] = help[i];
	}
}


void mergesort(int l, int r, int* b)
{
	if (l == r)
		return;
	int m = (r + l) / 2;
	mergesort(l, m, b);
	mergesort(m + 1, r, b);
	merge(l, m, r, b);
}

int main()
{
	srand((unsigned)time(NULL));
	int b[B_MAX];
	for (int i = 0; i < B_MAX; i++)
	{
		b[i] = rand() % 10;
		cout << b[i] << ' ';
	}
	cout << endl;
	mergesort(0, B_MAX - 1, b);
	for (int i = 0; i < B_MAX; i++)
	{
		cout << b[i] << ' ';
	}
	system("pause");
	return 0;
}

时间复杂度分析
使用master公式
T(N)=2T(N/2)+O(N)
a=2 b=2 d=1
符合log b a=d 所以归并排序的时间复杂度为O(N*logN)

02归并排序拓展

小和问题与逆序对问题

小和问题

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子 1,3,4,2,5
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1,3;
2左边比2小的数,1;
5左边比5小的数,1,3,4,2;
所以小和是这些数累加起来和为16
分析:求小和就是遍历当前数组,选择一个数,让这个数与左边的数依次比较,若左边的数小于当前数,则将该数累加到小和结果中。换种情况来考虑问题,求小和就是看当前数的右侧有几个数比当前数大,那么就加几倍的当前数。换个角度分析之后,时间复杂度还是一样的,我们使用归并排序的思想,使得在两组序列merge的时候,计算当前的小和。因为这两个系列都是有序的,所以不需要全部遍历,只需要计算右侧序列的下标即可。

		while (p1 <= m && p2 <= r) {//都不越界的时候
			res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
			//只有左组比右组小,才产生小和数量增加的行为,当前右组有多少个数比p1大
			help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
		}

在排序的同时记录小和结果,当右侧序列数大于当前左侧序列数的时候,小和增加(r - p2 + 1)倍的当前左侧数。当左组与右组的数相等时,需要先向下拷贝右组的数,因为不这样,不知道右组有几个数比当前数大。
完整代码

#include<iostream>
#define ARR_Length 5
using namespace std;
int merge(int l, int m, int r, int *b)
{
	int help[ARR_Length];
	int p1 = l;
	int p2 = m + 1;
	int i = 0;
	int result = 0;
	while (p1 <= m && p2 <= r)
	{
		result += b[p1] < b[p2] ? ((r - p2 + 1) * b[p1]) : 0;
		help[i++] = b[p1] > b[p2] ? b[p2++] : b[p1++];
	}
	while (p1 <= m)
	{
		help[i++] = b[p1++];
	}
	while (p2 <= r)
	{
		help[i++] = b[p2++];
	}
	for (i = 0; i < r - l + 1; i++)
	{
		b[i + l] = help[i];
	}
	return result;
}

int mergesort(int l, int r, int *b)
{
	if (l == r)
		return 0;
	int m = l + ((r - l) >> 1);

	return mergesort(l, m, b) //左侧排好并取小和
		+ mergesort(m + 1, r, b) //右侧排好并取小和
		+ merge(l, m, r, b);//merge时产生小和
}

int main()
{
	int b[ARR_Length] = { 1,3,4,2,5 };
	int a = 0;
	a = mergesort(0, ARR_Length - 1, b);
	cout << a;
	return 0;
}

逆序对

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序。
分析:找逆序的过程就是在归并的过程中,如果左边的数大于右边的数,那么计算左侧序列的下标,记录逆序数,然后将逆序数都打印出来
完整代码

#include<iostream>

#define B_Length 5
using namespace std;
int merge(int l, int m, int r, int b[])
{
	int help[B_Length];
	int p1 = l;
	int p2 = m + 1;
	int i = 0;
	int result = 0;
	while (p1 <= m && p2 <= r)
	{
		result += b[p1] > b[p2] ? (m - p1 + 1) : 0;
		if (b[p1] > b[p2])
			for (int i = p1; i <= m; i++) {
				cout << b[i] << b[p2] << endl;
			}
		help[i++] = b[p1] > b[p2] ? b[p2++] : b[p1++];
	}
	while (p1 <= m)
	{
		help[i++] = b[p1++];
	}
	while (p2 <= r)
	{
		help[i++] = b[p2++];
	}
	for (i = 0; i < r - l + 1; i++)
	{
		b[i + l] = help[i];
	}
	return result;
}

int mergesort(int l, int r, int b[])
{
	if (l == r)
		return 0;
	int m = l + ((r - l) >> 1);

	return mergesort(l, m, b) + mergesort(m + 1, r, b) + merge(l, m, r, b);
}

int main()
{
	int b[B_Length] = { 1,3,4 ,2,5 };
	int a = 0;
	a = mergesort(0, B_Length - 1, b);
	cout << a;
	system("pause");
	return 0;
}

03堆

  1. 堆结构就是用数组实现的完全二叉树结构。
  2. 完全二叉树中如果每棵树的最大值在顶部就是大根堆
  3. 完全二叉树中如果每棵树的最小值在顶部就是小根堆
  4. 堆结构的heapinsert和heapify操作
  5. 堆结构的增大和减小
  6. 优先级队列结构就是堆结构

什么是完全二叉树?二叉树是的,若是不满也是从左到右依次变满
数组从0出发的连续一段可以对应成完全二叉树(数组元素按二叉树行依次添加)
堆结构节点与数组对应位置关系
左孩子节点为2i+1,右孩子节点为2i+2 父节点为(i-1)/2

heapInsert:新节点插入进来,并向上调整形成大根堆的过程

分析:某个数现在处在index位置,往上继续移动,将当前数与父位置的数进行比较,若当前数大于父位置的数,则两个数交换位置,index更新到刚才父节点的位置。

//某个数现在处在index位置,往上继续移动
void heapinsert(int arr[],int index)
{
	while(arr[index] > arr[(index-1)/2])//若当前的数大于父位置的数
	{
		swap(arr[index],arr[(index-1)/2]);
		index = (index-1)/2;//换完后来到父位置,继续在循环中判断 直到不比父节点大 或到达根节点
	}
}

使用for循环遍历数组,调用heapinsert

for(int i = 0;i < length;i++ )
	{
		heapinsert(arr,i);//用for循环传入要处理的index
	}

时间复杂度分析
插入一个数需要比较数的高度次,也就是O(logN),N个数需要比较log1+log2+…+log(N-1)=O(N)

heapify 假设数组中某个值变小,重新将数组调整为大根堆的过程

分析:找到这个数的左右孩子的最大值,将左右孩子的最大值与这个数进行比较,
若孩子节点大于父节点,交换位置。
停止条件:左右孩子的最大值小于等于父节点或者 没有左右节点

//某个数在index位置,能否往下移动
void heapify(int arr[],int index,int heapsize)
{
	int left = index * 2 + 1; //左孩子的下标
	while(left < heapsize)
	{	//两个孩子中,谁的值大,把下标给large
		int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ? 
			left + 1:left;
		//父亲和较大孩子之间,谁的值大,就把下标给largest
		largest = arr[largest] > arr[index] ? largest : index;
		if(largest == index)//父节点就是三个节点中的最大值
			break;
		swap(arr[largest],arr[index]);
		index = largest;//向下移动
		left =  index * 2 + 1;
	}
}

04堆排序

  1. 先让整个数组都变成大根堆结构,建立堆的过程
    1.1 从上到下的方法,时间复杂度O(N*logN)(0-0位置是大根堆,依次插入数组元素进行heapInsert)
    1.2 从下到上的方法,时间复杂度为O(N)(先让最底层的节点heapify,再让上一层的,不断依次向上heapify)
  2. 把堆的最大值与堆末尾的值交换,然后减少堆的大小后,再去重新调整堆为大根堆,一直周而复始,时间复杂度为O(N*logN)
  3. 堆的大小减小为0的时候,排序完成。

排序代码

void heapsort(int arr[],int heapsize)
{
	while(heapsize > 0)
	{
		heapify(arr,0,heapsize);//O(logN) 0位置数往下heapify
		swap(arr[0],arr[--heapsize]);//O(1) 0位置与当前末位置交换
	}
}

堆排序完整代码

#include<iostream>
#include<time.h>
#define length 10
using namespace std;

void swap(int& a, int& b)
{
	int temp = a;
	a = b;
	b = temp;
}

void heapify(int arr[], int index, int heapsize)
{
	int left = 2 * index + 1;
	while (left < heapsize)
	{
		int largest = left + 1 < heapsize && arr[left + 1] > arr[left] ?
			left + 1 : left;
		largest = arr[index] < arr[largest] ? largest : index;
		if (index == largest)
			break;
		swap(arr[index], arr[largest]);
		index = largest;
		left = 2 * index + 1;
	}

}

void heapsort(int arr[], int heapsize)
{
	while (heapsize > 0)
	{
		heapify(arr, 0, heapsize);//O(logN) 0位置数往下heapify
		swap(arr[0], arr[--heapsize]);//O(1) 0位置与当前末位置交换
	}
}

void heapinsert(int arr[], int index)
{
	while (arr[index] > arr[(index - 1) / 2])
	{
		swap(arr[index], arr[(index - 1) / 2]);
		index = (index - 1) / 2;
	}
}


int main()
{
	srand((unsigned)time(NULL));

	int arr[length];
	int heapsize = length;
	cout << "arr = ";
	for (int i = 0; i < heapsize; i++)
	{
		arr[i] = rand() % 10;
		cout << arr[i] << " ";
		heapinsert(arr, i);
	}
	cout << endl << "arr1 = ";

	heapsort(arr, heapsize);

	for (int i = 0; i < length; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	//system("pause");
	return 0;
}

05堆排序扩展题目

已知一个几乎有序的数组,几乎有序是指如果数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
分析:将0-k位置的数置为小根堆,0位置必然是小根堆的最小值,将0位置弹出,将k+1位置的数插入,每次添加一个数就弹出一个数,没有数需要插入时,依次弹出小根堆中的数

排序java代码

public void sortedArrDistanceLessK(int[] arr, int k) {
		PriorityQueue<Integer> heap = new PriorityQueue<>();//默认为小根堆
		int index = 0;
		for (; index <= Math.min(arr.length, k); index++) {//把前k+1个数放到小根堆
			heap.add(arr[index]);
		}
		int i = 0;
		for (; index < arr.length; i++, index++) {//每次添加一个数,弹出一个数
			heap.add(arr[index]);
			arr[i] = heap.poll();
		}
		while (!heap.isEmpty()) {//没有数需要添加后,依次弹出小根堆中的数
			arr[i++] = heap.poll();
		}
	}

06荷兰国旗问题

问题一

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

分析:开辟一个小于等于num的区域,然后将这个区域的初始位置置于数组边界左侧,通过与数组元素的比较的过程,小于等于区域不断推着大于区域向右移动,直到将数组元素遍历完。
具体的比较规则
在这里插入图片描述
代码

#include<iostream>
#include<time.h>
#define arr_length 10
using namespace std;

//交换函数
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int arr[arr_length];
	int cur = 0;
	int num = 6;
	int x = -1;

	//生成随机长度为arr_length的数组,并输出
	srand((unsigned)time(NULL));
	cout<<"arr = ";
	for(int i = 0;i < arr_length;i++)
	{
		arr[i] = rand()%10;
		cout<<arr[i]<<" ";
	}
	cout<<endl;

	//核心代码
	while(cur < arr_length)
	{
		if(arr[cur] <= num)
		{
			swap(arr[cur++],arr[++x]);
		}
		else
		{
			cur++;
		}
	}
	
	//输出交换后的arr1,arr2
	cout<<"arr1 = ";
	for(int i = 0;i<=x;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;
	cout<<"arr2 = ";
	for(int i = x+1;i<arr_length;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;
	system("pause");
	return 0;
}

问题二

给定一个数组arr,和一个数num,请将小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

分析:再看问题二,两个问题的区别就是问题二中需要将等于num的数放在中间,那么我们可以在问题一的基础上再开辟一个大于num的区域,位于数组的右侧。
在这里插入图片描述
小于区域推着等于区域奔向大于区域,大于区域往左压缩待定位置
代码

#include<iostream>
#include<time.h>
#define arr_length 5
using namespace std;

//交换函数
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int arr[arr_length];
	int cur = 0;
	int num = 6;
	int x = -1;
	int y = arr_length;

	//生成随机长度为arr_length的数组,并输出
	srand((unsigned)time(NULL));
	cout<<"arr = ";
	for(int i = 0;i < arr_length;i++)
	{
		arr[i] = rand()%10;
		cout<<arr[i]<<" ";
	}
	cout<<endl;


	//核心代码
	while(cur < y)
	{
		if(arr[cur] < num)
		{
			swap(arr[cur++],arr[++x]);
		}
		else if(arr[cur] == num)
		{
			cur++;
		}
		else
		{
			swap(arr[cur],arr[--y]);
		}
	}


	//输出交换后的arr1,arr2,arr3
	cout<<"arr1 = ";
	for(int i = 0;i<=x;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;
	cout<<"arr2 = ";
	for(int i = x+1;i<y;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;
	cout<<"arr3 = ";
	for(int i = y;i<arr_length;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;


	system("pause");
	return 0;
}

07不改进的快速排序

快排1.0

基于问题一,整个数组中,拿最后一个数当做num
按规则划分区域后,将大于区域的第一个数与当前数(最后一个数 )交换位置
然后让左侧和右侧重复这个操作,每次递归都会有一个数排好位置
代码

#include<iostream>
#include<time.h>
#define length 10
using namespace std;

//交换函数
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int partition(int arr[],int l,int r)
{
	int num = arr[r];	//把最后一个数设为num进行比较
	int x = l-1;		//x为小于等于的区域,设为区域之前一个位置
	int cur = l;		//cur是当前遍历的指针
	while(cur < r+1)	//遍历所有位置
	{
		if(arr[cur]<=num)
		{
			swap(arr[cur++],arr[++x]);
		}
		else
			cur++;
	}

	return x-1;			
}



void quicksort(int arr[],int l,int r)
{
	if(l<r)
	{
		int a = partition(arr,l,r);
		quicksort(arr,l,a);
		quicksort(arr,a+1,r);
	}
}


int main()
{
	
	//准备随机数组
	int arr[length] ;
	srand((unsigned)time(NULL));
	cout<<"arr = ";
	for(int i = 0;i < length;i++)
	{
		arr[i] = rand()%10;
		cout<<arr[i]<<" ";
	}
	cout<<endl;

	//意外情况直接返回(本题用不到)
	if (arr == NULL || length < 2) 
	{
		return 0 ;
	}

	//核心
	quicksort(arr,0,length - 1);


	//结果输出
	cout<<"arr = ";
	for(int i = 0;i<length;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;

	system("pause");
	return 0 ;
}

快排2.0

分析快排1.0的缺点:每次只能找出一个等于num的数进行排序,如果存在多个相同的num还需要继续划分,多做了很多无用功。基于问题二荷兰国旗,每次可以搞定一批等于num的位置,会使时间复杂度的常数项大大降低。
代码

#include<iostream>
#include<time.h>
#define length 20
using namespace std;

void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

int* partition(int arr[],int l,int r,int a[])
{
	int x = l-1;
	int y = r+1;
	int cur = l;
	int num = arr[r];
	while(cur<y)
	{
		if(arr[cur]<num)
		{
			swap(arr[cur++],arr[++x]);
		}
		else if(arr[cur]>num)
		{
			swap(arr[cur],arr[--y]);
		}
		else
		{
			cur++;
		}
		
	}
	a[0] = x;
	a[1] = y;
	return a;
}

void quicksort(int arr[],int l,int r)
{
	int a[2] = {0};
	if(l<r)
	{
		int *p = partition(arr,l,r,a);
		quicksort(arr,l,*p);
		quicksort(arr,*(p+1),r);
	}
}

int main()
{
	//生成随机数组
	int arr[length];
	srand((unsigned)time(NULL));
	cout<<"arr = ";
	for(int i = 0;i<length;i++)
	{
		arr[i] = rand()%10;
		cout<<arr[i]<<" ";
	}
	cout<<endl;
	
	quicksort(arr,0,length - 1);
	
	//打印结果
	cout<<"arr1 = ";
	for(int i = 0;i<length;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;

	system("pause");
	return 0;
}

08 随机快速排序

分析:荷兰国旗的快排2.0版本的缺点在于使用最后一个数作为num,那么此时的复杂度便于数据状况有关,若原数组的本来就是有序的,那么此时的复杂度便很高。如果从数组元素中随机选择一个数,就可以绕开原始数据状况,时间复杂度变为长期的期望值。
选择数据组随机一个数int num = arr[l+rand()%(r-l+1)];
代码

#include<iostream>
#include<time.h>
#define length 20

using namespace std;
void swap(int &a,int &b)
{
	int temp = a;
	a = b;
	b = temp;
}
//这是一个处理arr[l..r]的函数
//返回一个长度为2的数组
int* partition(int arr[],int l,int r,int a[])
{
	int x = l-1;//返回数组的左边界
	int y = r+1;//返回数组的右边界
	int cur = l;
	int num = arr[l+rand()%(r-l+1)];//等概率随机选一个位置,与最右侧的数交换
	while(cur<y)
	{
		if(arr[cur]<num)
		{
			swap(arr[cur++],arr[++x]);
		}
		else if(arr[cur]>num)
		{
			swap(arr[cur],arr[--y]);
		}
		else
		{
			cur++;
		}
		
	}
	a[0] = x;
	a[1] = y;
	return a;
}

void quicksort(int arr[],int l,int r)
{
	int a[2] = {0};
	if(l<r)
	{
		int *p = partition(arr,l,r,a);
		quicksort(arr,l,*p);
		quicksort(arr,*(p+1),r);
	}
}


int main()
{
	//生成随机数组
	int arr[length];
	srand((unsigned)time(NULL));
	cout<<"arr = ";
	for(int i = 0;i<length;i++)
	{
		arr[i] = rand()%100;
		cout<<arr[i]<<" ";
	}
	cout<<endl;


	quicksort(arr,0,length - 1);


	//打印结果
	cout<<"arr1 = ";
	for(int i = 0;i<length;i++)
	{
		cout<<arr[i]<<" ";
	}
	cout<<endl;

	system("pause");
	return 0;
}

随机选择一个数,与最后一个数交换,然后开始划分区域。最好情况与最坏的情况都是概率事件。每种事件都是1/n,概率与事件复杂度相乘累加求得期望得时间复杂度是O(N*logN)额外空间复杂度为O(logN)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值