剑指Offer读书笔记(3)

2.4.1 排序和查找

查找相对简单,查找分为顺序查找、二分查找、哈希表查找和二叉排序树查找

PS:哈希表最主要的优点是我们利用它能够在O(1)的时间度查找某一元素,是效率最高的查找方式。但缺点在于需要额外的空间来实现哈希表。

排序比较复杂,排序分为插入排序,哈希排序,冒泡排序,选择排序,堆排序,归并排序,快速排序

PS:比较排序的优缺点应从额外空间消耗,平均时间复杂度和最差时间复杂度等方面比较。

 

快速排序:实现快速排序的关键在于先在数组中选择一个数字,接下来把数组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择的数字大的数字移到数组的右边。

 具体实现如下:

int Partition(int data[], int length, int start, int end)
{
	if (data != NULL || length <= 0 || start < 0 || end >= length)
		throw new std::exception("Invalid Parameters");
	int index = RandomInRange(start, end);//用来生成一个在start和end之间的随机数
	Swap(&data[index], &data[end]);		//交换两个数字

	int small = start - 1;
	for (index = start; index < end; ++index)
	{
		if (data[index] < data[end])
		{
			++small;
			if (small != index)
				Swap(&data[index], &data[small]);
		}
	}
	++small;
	Swap(&data[small], &data[end]);

	return small;
}
之后便可以根据递归的思路来实现对每次选中的数字的左右两边进行排序。

void QuickSort(int data[], int length, int start, int end)
{
	if (start == end)
		return;

	int index = Partition(data, length, start, end);
	if (index > start)
		QuickSort(data, length, start, index - 1);
	if (index < end)
		QuickSort(data, length, index + 1, end);
}
 在前面的代码中,函数Partition除了可以用在快速排序中还可以用来实现在 长度为n的数组中查找第k大的数字

排序在实际问题中的应用:对某公司的员工的年龄排序,员工数量在几万名,要求时间复杂度为O(n)。

在实现之前需要认识到需要排序的数字是在一个较小的范围内,位放方便实现可以考虑利用辅助内存。

void SoftAges(int ages[], int length)
{
	if (ages == NULL || length <= 0)
		return;

	const int OldestAge = 99;
	int timesOfAge[OldestAge + 1];

	for (int i = 0; i <= OldestAge; ++i)//统计每个年龄出现的次数
		timesOfAge[i] = 0;
	for (int i = 0; i < length; ++i)
	{
		int age = ages[i];
		if (age<0 || age>OldestAge)
			throw new std::exception("age out of range.");

		++timesOfAge[age];
	}

	int index = 0;
	for (int i = 0; i <= OldestAge; ++i)
	{
		for (int j = 0; j < timesOfAge[i]; ++j)
		{
			ages[index] = i;//某个年龄出现了几次,就重置几次该年龄,相当于给数组ages排序
			++index;
		}
	}
}
该方法利用长度为100 的数组作为辅助空间换来了O(n)的时间效率。


查找的实际用例:旋转数组的最小数,例如:{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

该题的直观思路不难,从头到尾遍历一遍数组,找出最小数,时间复杂度为O(n),但通过观察可以看出,数组在旋转后可以别分为两部分,并且都是有序的,前半部分的数值整体上都比后半部分的大,以此可以进行优化,找出比时间复杂度O(n)更优的解。

int minArray(int* a, int length)
{
	if (a == NULL || length <= 0)
	{
		throw new std::exception("Invalid parameters.");
	}

	int index1 = 0;
	int index2 = length - 1;
	int indexMid = index1;
	while (a[index1] >= a[index2])
	{
		if (index2 - index1 == 1)
		{
			return a[index2];
		}
		indexMid = (index1 + index2) / 2;
		if (a[index1] <= a[indexMid])
		{
			index1 = indexMid;
		}
		else if (a[index2] >= a[indexMid])
		{
			index2 = indexMid;
		}
	}
	return a[indexMid];
}
上面的代码存在着很大的缺陷,例如当输入特殊的数组,数组内存在大量相等且连序的数值时,如{1,1,1,0,1,1}按照上面的解法便会出现指针更改移动的问题,是指针的指向不在明确,从而导致导致程序崩溃,此时要找除最小值便只能顺序遍历数组。

int MinArray(int* a, int length)
{
	if (a == NULL || length <= 0)
	{
		throw new std::exception("Invalid parameters.");
	}

	int index1 = 0;
	int index2 = length - 1;
	int indexMid = index1;
	while (a[index1] >= a[index2])
	{
		if (index2 - index1 == 1)
		{
			indexMid = index2;
			break;
		}

		indexMid = (index1 + index2) / 2;
		if (a[index1] == a[index2] && a[index1] == a[indexMid])
		{
			return MinInOrder(a, index1, index2);
		}
		if (a[index1] <= a[indexMid])
		{
			index1 = indexMid;
		}
		else if (a[index2] >= a[indexMid])
		{
			index2 = indexMid;
		}
	}
	return a[indexMid];
}
int MinInOrder(int* a, int index1, int index2)		//当数组中年出现大量连续且相等的数值时的处理
{
	int result = a[index1];
	for (int i = 0; i <= index2; i++)
	{
		if (result > a[i])
			return result;
	}
	return result;
}

2.4.2递归和循环

递归是在一个函数的内部调用这个函数的本身。

循环是通过设置计算的初始值及终止条件,在一个范围内重复运算。

用递归实现的算法通常会比循环简洁,但相应的,递归的每次调用都会在时间和空间的消耗比较大。每次递归都需要在内存栈中分配空间以保存参数、返回地址和临时变量,而且往栈内压入数据和弹出数据都需要时间。

递归计算的时间复杂度是以n的指数递增的。

递归的本质是把一个问题分解成两个或多个小问题。

斐波那契数列的递归和非递归解法

long long Fibonacci(size_t n)
{
	if (n <= 0)
		return 0;

	if (n == 1)
		return 1;

	return Fibonacci(n - 1) + Fibonacci(n - 2);
}
递归实现的代码简洁,但仔细考虑递归的每个过程会发现对大量相同数据的存储和操作时递归的最大问题,当N值过大时这些消耗便不能被忽视。

long long Fibonacci(size_t n)
{
	int result[2] = { 0, 1 };
	if (n < 2)
	{
		return result[n];
	}

	long long first = 0;
	long long second = 1;
	long long fibN = 0;
	for (size_t i = 0; i <= n; i++)
	{
		fibN = first + second;

		first = second;
		second = fibN;
	}
	return fibN;
}

2.4.3位运算

位运算是把数字用二进制表示后,对每一位上的0或1的运算。

位与(&)运算:0&0=0;0&1=0;1&0=0;1&1=0

位或(|)运算:0|0=0;0|1=1;1|0=1;1|1=1

位异或(^)运算:0^0=0;1^0=1;0^1=1;1^1=1

位左移(<<):左移n位时,最左边的位将被丢弃,同时在最右边补上n位0。

位右移(>>):右移n位时,当数字是一个无符号数时,则用0填补最左边n位;如果数字是有符号数,则用数字的符号位填补最左边的n位。

联系题:求二进制中1的个数

//可能引起死循环的算法,原因:没有考虑到n 为负数的情况,N为负数时向右移位高位补1而不是0
//不能用除2来代替N向右移位,因为除法效率很低
int Num(int n)
{
	int count = 0;
	while (n)
	{
		if (n & 1)
			count++;

		n = n >> 1;
	}
	return n;
}
//常规解法
int Num(int n)
{
	int count = 0;
	size_t flag = 1;
	while (flag)
	{
		if (n&flag)
			count++;

		flag = flag << 1;
	}
	return count;
}
//最优解
//思想:把一个数减去1,再和原来的数相&,会把该数二进制的最右边一个1变为0;依次循环比较高效
int Num(int n)
{
	int count = 0;

	while (n)
	{
		++count;

		n = (n - 1)&n;
	}
	return count;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值