时间复杂度为O(n*logn)的排序算法

一、时间复杂度为O(n*logn)的排序算法

二、今日刷题

一、时间复杂度为O(n*longn)的排序算法

1、归并排序:
排序的思想:就是不断分成两部分:左边和右边,分别将左边和右边有序之后才进行合并得到最后的有序的数组。


时间复杂度:o(n*logn)。


核心:归并排序之所以能达到o(n*logn)的时间复杂度,主要原因是其避免了很多不必要的排序,当保持左边和右边部分有序之后,其进行合并的时候进行比较的是不同组的元素之间的比较,组内的元素不再进行比较。

代码:

void merge(int a[], int l, int mid, int r)
{
	int* help = new int[r - l + 1];//辅助数组
	int p1 = l;
	int p2 = mid + 1;
	int k = 0;
	while (p1 <= mid&&p2 <= r)
	{
		if (a[p1] <= a[p2])
		{
			help[k++] = a[p1];
			p1++;
		}
		else
		{
			help[k++] = a[p2];
			p2++;
		}
	}
	while (p1 <= mid)
		help[k++] = a[p1++];
	while (p2 <= r)
		help[k++] = a[p2++];
	int len = r - l + 1;
	for (int i = 0; i < len; ++i)
		a[l + i] = help[i];
	delete[] help;
	//cout << "hello" << endl;
}
void mergesort(int a[], int l, int r)
{
	if (l == r)
		return;
	int mid = l + ((r - l) >> 1);
	mergesort(a, l, mid);
	mergesort(a, mid + 1, r);
	merge(a, l, mid, r);
}

例题:最小和问题和求逆序对,以最小和问题为例进行分析:

最小和问题:最小和也就是每个数的左边比其小的数的和,再将数组中所有的该和进行累加的结果,例如数组{3,5,2,6},a[0]左边没有比起小的,a[1]左边比其小的为3,a[2]左边比其小的没有,a[3]左边比其小的有3,5,2,所以数组的最小和为0+3+0+3+5+2.

分析:求最小和可以转化为求一个数的右边有多少个比其大的数,有几个那么其就贡献了几次最小和。求一个数右边有多少个比其大的使用归并排序的思想。在归并排序的merge的时候进行求解,核心思想是归并后的部分内部不必进行计算,只有不同部分之间才涉及计数;当出现相等的值的时候,先防止右边的数。

代码:

int merge(int a[], int l, int mid, int r)
{
	int* help = new int[r - l + 1];//辅助数组
	int p1 = l;
	int p2 = mid + 1;
	int k = 0;
	int res = 0;
	while (p1 <= mid&&p2 <= r)
	{
		if (a[p1] < a[p2])//左边的数字小,参与贡献
		{
			help[k++] = a[p1];
			res += (r - p2 + 1)*a[p1];
			p1++;
		}
		else//左边的数字大,那么就不参与贡献
		{
			help[k++] = a[p2];
			p2++;
		}
	}
	while (p1 <= mid)
		help[k++] = a[p1++];
	while (p2 <= r)
		help[k++] = a[p2++];
	int len = r - l + 1;
	for (int i = 0; i < len; ++i)
		a[l + i] = help[i];
	delete[] help;
	return res;
	//cout << "hello" << endl;
}
int mergesort(int a[], int l, int r)
{
	if (l == r)//递归结束的条件,注意不要忽略
		return 0;
	int mid = l + ((r - l) >> 1);
	return mergesort(a, l, mid)+mergesort(a, mid + 1, r)+merge(a, l, mid, r);
	//这行代码的理解个人觉得很重要
	//首先将可以直接将左边家上右边再加上总体合并的,可以直接将merge(a,l,mir,r)相加的原因是
	//同一个部分内部已经求解过了,也就是mergesort的部分,那么现在值涉及不同部分之间,所以直
	//接加上不同部分合并的结果即可
}

2、堆排序(以大根堆为例)

堆结构其实就是一个数组,只不过将该数组视为一棵完全二叉树,对于数组中下标为i的节点,其左孩子的下标为2i+1,右孩子的下标为2i+2(数组的下标从0开始),父亲节点的下标为(i-1)/2。
堆结构中重要的操作有两个:heapinsert和headpify。heapinsert是指我们在堆中插入一个新的数字的时候,也就是在数组的末尾新增一个数字的时候将数据变成堆。
heapify是将堆顶元素删除,将剩下的数据重新变为堆。

小技巧:由于数组的长度和数据的数量一般是固定的,所以我们可以使用一个单独的变量heapsize来指定堆的大小。

代码:

void heapinsert(int a[], int index)//在数组的index位置上新增加一个数
{
	while (a[index] > a[(index - 1) / 2])//这里已经考虑了边界条件,如果是到0,那么0和0自己比较就会跳出循环
	{
		int tmp = a[index];
		a[index] = a[(index - 1) / 2];
		a[(index - 1) / 2] = tmp;
		index = (index - 1) / 2;
	}
}

void heapify(int a[], int index,int size)
{
	int left = index * 2 + 1;
	int largest;
	//在进行操作之前先判断是否有左孩子和右孩子
	while (left < size)
	{
		int largest = left + 1 < size&&a[left + 1] > a[left]?left + 1 : left;
		largest = a[largest]>a[index] ? largest : index;
		if (largest == index)
			break;
		int tmp = a[largest];
		a[largest] = a[index];
		a[index] = tmp;
		index = largest;
		left = index * 2 + 1;
	}
}

堆排序:有了上述的操作之后,堆排序就简单了,我们使用heapinsert逐个将数组的元素加入堆,也就是不断扩大对的大小,将数组符合最大堆的情况。然后每次将堆顶取出,之后用最后的元素代替该元素,然后在heapify将剩下的又调整成最大堆,每次都得到最大值,所以最后得到的就是从大到小排好序的。

进行数组的更新:如果要进行数组的更新,那么先看看能不能heapinsert,之后看能不能headpify。

由给定数组建堆:
方法一:时间复杂度为o(nlogn),这对数组中的数据是一个一个给的或者是一次性给的都适用,具体的方法就是对每一个数组中的数字从头到尾进行heapinsert。
方法二:时间复杂度为o(logn),这只针对的是数组是一次性给定的,我们先假定给定的数组就是一个大根堆,然后对堆中的每一个位置进行heapify,也就是判断自己与其子树是否构成大根堆,那么最底层有n/2个结点,其向下的层数就是其本身,倒数第二层有n/4,其操作2层……那么总的就是n/2+n/4
2+n/8*3……,最后求的复杂度为o(n)。

函数类库提供的堆:在C++中我们可以使用优先级队列,其就是一个堆:

//默认的就是大根堆
priority_queue<int> a;
//小根堆要指明比较其
priority_queue<int,vector<int>,greater<int> > b;

例题:
题目:已知一个几乎有序的数组,几乎有序是指如果把数组排好顺序的话,每个元素移动的距离不超过k,并且k相对于数组来说比较小。请选择一个适合的排序算法针对这个数据进行排序。
思路:使用小根堆(大根堆也是可以的),首先将0到k的k+1个数进入小根堆,由于距离不超过k,所以到0位置上的数一定在这个k+1个数之间,然后在对1到k+2上的数进小根堆得到的堆顶就是1位置上的数,依次就可以得到最后有序的数组。
时间复杂度:每次调整堆的时间复杂度为o(logK),一共有n个数,所以总的时间复杂度为o(n*logK)。
代码:

void sortedArrayDistanceLessK(vector<int> a, int k)
{
	priority_queue<int, vector<int>, greater<int> > heap;
	int len = a.size();
	//先将0到k-1的数组进堆
	int index = 0;
	for (; index < min(len, k); index++)
		heap.push(a[index]);
	int i = 0;
	for (; index < len; i++, index++)
	{
		heap.push(a[index]);
		a[i] = heap.top();
		heap.pop();
	}
	while (!heap.empty())
	{
		a[i++] = heap.top();
		heap.pop();
	}

}

3、快排之partition:
快排的核心思想就是partition,而所谓partition也就是给定一个数组和一个值,将数组小于给定值的放在数组的左边,大于该值的放在数组的右边。要求时间复杂度为o(n),空间复杂度为o(1)。

思路:设置一个小于等于区域,对于数组的当前的数,如果其小于等于给定的数那么其就与小于等于区域的下一个数交换,否则直接跳下一个数。从数的位置可以直观理解:数组中的数:小于等于区域---->大于区域----->当前的值。

例题:给定数组和一个值,将小于该值的放在数组的左边,等于该值的放在数组的中间,大于该值的放在数组的右边。
思路:仍然指定小于区域,等于区域和大于区域;当前的值如果小于给定的值,那么就让其与小于区域的下一个数交换,并且将当前值前进一个数;如果当前值等于给定值直接将当前值前进一个数;如果大于将当前值与大于区域的前一个值交换,当前的位置不变。从数组的位置直观理解就是:小于区域的值------>等于区域的值------->当前值------->大于区域的值。
代码:

void partition(int a[], int l, int r,int label)
{
	int less = l - 1;
	int more = r + 1;
	int tmp;
	while (l < more)//当当前的位置和大于区域相遇的时候结束
	{
		if (a[l] < label)
		{
			tmp = a[l];
			less++;
			a[l] = a[less];
			a[less] = tmp;
			l++;
		}
		else if (a[l]>label)
		{
			tmp = a[l];
			more--;
			a[l] = a[more];
			a[more] = tmp;
			//当前位置不变
		}
		else
			l++;
	}
}

二、今日刷题

题目:用两个栈实现一个队列的功能
思路:队列和栈之间的区别在于,队列是先进先出,栈是后进先出。这里两个栈为s1和s2,实现队列的出队列功能:s1如果有元素的话,就将s1的元素压倒s2中,然后弹出s2栈顶的元素,如果s1是空s2有元素,那么直接弹出s2栈顶的元素;如果两个都是空的,那么报错。实现入队列,如果s2中有元素,先将s2中的元素入到s1中,然后再将新的元素push进s1中,否则直接push进s1,代码为:
class Solution
{
    //push的时候如果s2是空的,那么直接push进s1,如果s2非空,那么就要先将s2中的元素push如s1
    //pop的时候,如果s2非空,那么直接pop,如果s2为空,那么将s1中的push如s2,然后再pop
public:
    void push(int node) {
        int tmp;
        if(!s2.empty())
        {
            tmp = s2.top();
            s2.pop();
            s1.push(tmp);
        }
        s1.push(node);
    }

    int pop() {
        int tmp;
        if(!s2.empty())
        {
            tmp = s2.top();
            s2.pop();
            return tmp;
        }
        else if(s1.empty())
        {
            cout<<"erroe!";
            return -1;
        }
        else
        {
            while(!s1.empty())
            {
                tmp = s1.top();
                s1.pop();
                s2.push(tmp);
            }
            tmp = s2.top();
            s2.pop();
            return tmp;
        }
    }

private:
    stack<int> s1;
    stack<int> s2;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值