算法与错题笔记

算法

基本排序算法

总结

排序算法平均时间复杂度最好情况最坏情况空间复杂度排序方式稳定性
冒泡排序 O ( n 2 ) \textbf{O}(n^2) O(n2) O ( n ) \textbf{O}(n) O(n) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( 1 ) \textbf{O}(1) O(1)In-place稳定
选择排序 O ( n 2 ) \textbf{O}(n^2) O(n2) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( 1 ) \textbf{O}(1) O(1)In-place不稳定
插入排序 O ( n 1.3 ) \textbf{O}(n^{1.3}) O(n1.3) O ( n ) \textbf{O}(n) O(n) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( 1 ) \textbf{O}(1) O(1)In-place稳定
希尔排序 O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( 1 ) \textbf{O}(1) O(1)In-place不稳定
归并排序 O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n ) \textbf{O}(n) O(n)Out-place稳定
快速排序 O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( log ⁡ n ) \textbf{O}(\log n) O(logn)In-place不稳定
堆排序 O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn) O ( 1 ) \textbf{O}(1) O(1)In-place不稳定
计数排序 O ( n + k ) \textbf{O}(n+k) O(n+k) O ( n + k ) \textbf{O}(n+k) O(n+k) O ( n + k ) \textbf{O}(n+k) O(n+k) O ( n + k ) \textbf{O}(n+k) O(n+k)Out-place稳定
桶排序 O ( n + k ) \textbf{O}(n+k) O(n+k) O ( n ) \textbf{O}(n) O(n) O ( n 2 ) \textbf{O}(n^2) O(n2) O ( n + k ) \textbf{O}(n+k) O(n+k)Out-place稳定
基数排序 O ( n × k ) \textbf{O}(n\times k) O(n×k) O ( n × k ) \textbf{O}(n\times k) O(n×k) O ( n × k ) \textbf{O}(n\times k) O(n×k) O ( n + k ) \textbf{O}(n+k) O(n+k)Out-place稳定
  • 平均时间复杂度同样是 O ( n log ⁡ n ) \textbf{O}(n\log n) O(nlogn),为什么快速排序要比堆排序性能好?
    • 堆排序访问数据的方式没有快速排序友好
      • **对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。**比如,堆排序中,最重要的一个操作就是数据的堆化。比如下面这个例子,对堆顶进行堆化,会依次访问数组下标是1,2,4,8的元素,而不像快速排序那样,局部顺序访问,所以,这样对CPU缓存是不友好的
    • 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序
      • 对于基于比较的排序算法来说,整个排序过程是由两个基本操作组成的,比较和交换。快速排序交换的次数不会比逆序度多。但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对选择顺序,导致数据有序度降低。比如对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了

冒泡排序

// 基本冒泡排序
void bubbleSort(int array[], int length)
{
	for (int i = 0; i < length - 1; i++)
	{
		for (int j = 0; j < length - 1 - i; j++)
		{
			if (array[j] > array[j + 1])
			{
				swap(array[j], array[j + 1]);
			}
		}
	}
}

// 改进版冒泡排序
void improvedBubbleSort(int array[], int length)
{
	bool flag = true;
	for (int i = 0; i < length - 1 && flag == true; i++)
	{
		flag = false;
		for (int j = 0; j < length - 1 - i; j++)
		{
			if (array[j] > array[j + 1])
			{
				flag = true;
				swap(array[j], array[j + 1]);
			}
		}
	}
}

选择排序

// 选择排序
void selectionSort(int array[], int length)
{
	for (int i = 0; i < length - 1; i++)
	{
		int minIdx = i;
		for (int j = i + 1; j < length; j++)
		{
			if (array[j] < array[minIdx])
			{
				minIdx = j;
			}
		}
		if (minIdx != i)
		{
			swap(array[i], array[minIdx]);
		}
	}
}

插入排序

  • 最佳情况在 排序数组有序 时发生
// 插入排序
void insertionSort(int array[], int length)
{
	for (int i = 1; i < length; i++)
	{
		int j = i - 1;
		int tmp = array[i];
		while (j >= 0 && array[j] > tmp)
		{
			array[j + 1] = array[j];
			j--;
		}
		array[j + 1] = tmp;
	}
}

// 二分插入排序
void binaryInsertionSort(int array[], int length)
{
	for (int i = 1; i < length; i++)
	{
		int tmp = array[i];
		int low = 0, high = i - 1;
		while (low <= high)
		{
			int mid = low + (high - low) / 2;
			if (array[mid] > tmp)
				high = mid - 1;
			else
				low = mid + 1;
		}
		for (int j = i; j >= high + 2; j--)
		{
			array[j] = array[j - 1];
		}
		array[high + 1] = tmp;
	}
}

希尔排序

// 希尔排序
void shellSort(int array[], int length)
{
	std::vector<int> gaps;
	int h = 1;
	while (h < length)
	{
		gaps.push_back(h);
		h = 3 * h + 1;
	}
	int numGap = gaps.size()-1;
	for (; numGap >= 0; numGap--)
	{
		int gap = gaps[numGap];
		for (int i = gap; i < length; i += gap)
		{
			shellInsert(array, gap, i);
		}
	}
}
// 插入操作
void shellInsert(int array[], int gap, int idx)
{
	int tmp = array[idx];
	int j = idx - gap;
	while (j >= 0 && array[j] > tmp)
	{
		array[j + gap] = array[j];
		j -= gap;
	}
	array[j + gap] = tmp;
}

归并排序

// 归并排序
void mergeSort(int array[], int length)
{
	//mergeSort_recursive(array, 0, length - 1);	// 递归版本
	mergeSort_iterative(array, length);  	// 迭代版本
}

// 迭代版本
void mergeSortIterative(int array[], int length)
{
	int stride = 2;
	while (stride<length)
	{
		int start = 0;
		while (start + stride < length)
		{
			merge(array, start, start + stride - 1);
			start += stride;
		}
		merge(array, start, length-1);
		stride *= 2;
	}
	merge(array, 0, length - 1);
}

// 递归版本
void mergeSortRecursive(int array[], int first, int last)
{
	if (first < last)
	{
		int mid = first + (last-first) / 2;
		mergeSortRecursive(array, first, mid);
		mergeSortRecursive(array, mid + 1, last);
		merge(array, first, last);
	}
}

void merge(int array[], int first, int last)
{
    if(first<last)
    {
        int left = first, mid = first + (last - first) / 2, right = mid + 1, idx = 0;
        int* tmp = new int[static_cast<size_t>(last-left)+1];
        while (left<=mid||right<=last)
        {
            if (left <= mid && right <= last)
            {
                if (array[left] <= array[right])
                    tmp[idx++] = array[left++];
                else
                    tmp[idx++] = array[right++];
            }
            else if (left <= mid)
                tmp[idx++] = array[left++];
            else
                tmp[idx++] = array[right++];
        }
        idx = 0;
        while (first <= last)
            array[first++] = tmp[idx++];
        delete[] tmp;
    }
}

快速排序

  • 最坏情况:每次划分只能将序列分为一个元素与其他元素两部分(正序,逆序,元素全部相等),这时的快速排序退化为冒泡排序
  • 最好情况:Partition函数每次恰好能均分序列
  • 与枢轴(pivot)的选择策略有关
// 递归版本快排
// 
void quickSortRecurcive(int array[], int low, int high)
{
	if (low < high)
	{
		int pivotIdx = partition(array, low, high);
		quickSortRecurcive(array, low, pivotIdx-1);
		quickSortRecurcive(array, pivotIdx+1, high);
	}
}

// 迭代版本
void quickSortIterative(int array[], int length)
{
    std::stack< std::pair<int,int> > range;
    int left, right, mid;
    if(length>1)
    {
        left = 0;
        right = length - 1;
    	mid = partition(array, left, right);
        if(left < mid-1)
            range.push({left, mid-1});
        if(mid+1 < right)
            range.push({mid+1, right});
    }
    while(!range.empty())
    {
        auto subRange = range.top();
        range.pop();
        left = subRange.first;
        right = subRange.second;
        mid = partition(array, left, right);
        if(left < mid-1)
            range.push({left, mid-1});
        if(mid + 1 < right)
            range.push({mid+1, right});
    }
}

int partition(int array[], int low, int high)
{
	// swap(array[low], array[(low + high) / 2]);   // 选中间元素作为枢轴
    // int pivot = array[low];		// 选第一个元素作为枢轴
    
	// 三数取中(median-of-three)
    int pivot = selectPivotMedianOfThree(array, low, high);
	while (low < high)
	{
		while (low < high && array[high] >= pivot)
			high--;
		array[low] = array[high];
		while (low < high && array[low] <= pivot)
			low++;
		array[high] = array[low];
	}
	array[low] = pivot;
	return low;
}


// 函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴
int selectPivotMedianOfThree(int arr[],int low,int high)
{
	int mid = low + ((high - low) >> 1);// 计算数组中间的元素的下标
	//使用三数取中法选择枢轴
 
	if (arr[mid] > arr[high]) // 目标: arr[mid] <= arr[high]
	{
		swap(arr[mid],arr[high]);
	}
 
	if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
	{
		swap(arr[low],arr[high]);
	}
 
	if (arr[mid] > arr[low]) // 目标: arr[low] >= arr[mid]
	{
		swap(arr[mid],arr[low]);
	}
 
	//此时,arr[mid] <= arr[low] <= arr[high]
	return arr[low];
	// low的位置上保存这三个位置中间的值
	// 分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}

堆排序

void heapSort(int array[], int length)
{
	buildHeap(array, length);
	for (int i = length - 1; i > 0; i--)
	{
		swap(array[0], array[i]);   // 将最大值移动到最后,然后调整堆
		adjustHeap(array, 0, i - 1);
	}
}

void buildHeap(int array[], int length)
{
	for (int i = length / 2; i >= 0; i--)  // 自底向上
		adjustHeap(array, i, length - 1);
}
// 下沉法(sink)
void adjustHeap(int array[], int root, int last)
{
	int larger = 2 * root + 1;  // 左子节点
	while (larger <= last)  // 至少一个孩子
	{
		if (larger < last && array[larger] < array[larger + 1])  // have tow children, and the right 
			larger++;											// child is larger than the left, 
		if (array[root] < array[larger])						//"larger" points to the right child
		{
			swap(array[root], array[larger]);
			root = larger;
			larger = 2 * root + 1;   // go to next level
		}
		else
			break;   //satisfy the heap property, exit
	}
}

计数排序

// 计数排序
void countSort(int arr[], int length)
{
    int minVal = INT_MAX, maxVal = INT_MIN;
    for(int i = 0; i < length; i++)
    {
        if(arr[i] > maxVal)
        	maxVal = array[i];
        if(arr[i] < minVal)
            minVal = arr[i];
    }
    int bias = 0 - minVal;
    int * count = new int[maxVal - minVal + 1];
    memset(count, 0, maxVal-minVal+1);
    for(int i = 0; i < length; i++)
    {
        count[arr[i] + bias]++;
    }
    
    for(int i = 1; i < maxVal-minVal+1; i++)
    {
        count[i] += count[i-1];
    }
    
    int * result = new int[length];
    int idx = length-1;
    while(idx>=0)
    {
        count[array[idx] + bias]--;
        result[count[arr[idx] + bias] - 1] = arr[idx];
        idx--;
    }
    for(int i = 0; i<length;i++)
        arr[i] = result[i];
    
    delete count;
    delete result;
}

桶排序

void bucketSort(int array[], int length)
{
    int maxVal = INT_MIN;
    int minVal = INT_MAX;
    for(int i = 0; i < length; i++)
    {
        maxVal = max(maxVal, array[i]);
        minVal = min(minVal, array[i]);
    }
    int bucketCount = (maxVal - minVal)/lenght + 1;
    std::vector<int> *bucketArr = new std::vector<int>[bucketCount];
    for(int i = 0; i < length; i++)
    {
        int num = (array[i] - minVal) / length;
        bucketArr[num].push_back(array[i]);
    }
    for(int i = 0; i < bucketCount; i++)
    {
        std::sort(bucketArr[i].begin(), bucketArr[i].end());
    }
    int idx = 0;
    for(int i = 0; i < bucketCount; i++)
    {
        for(int j = 0; j < bucketArr[i].size(); j++)
            array[idx++] = bucketArr[i][j];
    }
    delete [] bucketArr;
}

哈希

  • 概念

    • 散列的概念属于查找,它不以关键字的比较为基本操作,采用直接寻址技术。在理想情况下,查找的期望时间为O(1)
    • hash函数就是把任意长的输入字符串变化成固定长的输出字符串的一种函数。输出字符串的长度称为hash函数的位数
    • 散列(Hashing)通过散列函数将要检索的项与索引(散列,散列值)关联起来,生成一种便于搜索的数据结构(散列表)
  • 常用哈希构造函数

    • 直接定址法
      • 取关键字或关键字的某个线性函数值为哈希地址:H(key) = key 或 H(key) = a·key + b,其中a和b为常数,这种哈希函数叫做自身函数
      • 注意:由于直接定址所得地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际中能使用这种哈希函数的情况很少
    • 相乘取整法
      • 首先用关键字key乘上某个常数A(0 < A < 1),并抽取出key*A的小数部分;然后用m乘以该小数后取整
      • 注意:该方法最大的优点是m的选取比除余法要求更低。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取 0.61803……
    • 平方取中法
      • 取关键字平方后的中间几位为哈希地址
      • 通过平方扩大差别,另外中间几位与乘数的每一位相关,由此产生的散列地址较为均匀。这是一种较常用的构造哈希函数的方法
      • 将一组关键字(0100,0110,1010,1001,0111),平方后得(0010000,0012100,1020100,1002001,0012321),若取表长为1000,则可取中间的三位数作为散列地址集:(100,121,201,020,123)
    • 除留余数法
      • 取关键字被数p除后所得余数为哈希地址:H(key) = key MOD p (p ≤ m)
      • 注意:这是一种最简单,也最常用的构造哈希函数的方法。它不仅可以对关键字直接取模(MOD),也可在折迭、平方取中等运算之后取模。值得注意的是,在使用除留余数法时,对p的选择很重要。一般情况下可以选p为质数或不包含小于20的质因素的合数
    • 随机数法
      • 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random (key),其中random为随机函数。通常,当关键字长度不等时采用此法构造哈希函数较恰当
  • 解决哈希冲突的方法

    • 开放定址法:
      • 就是在发生冲突后,通过某种探测技术,去依次探查其他单元,直到探查到不冲突为止,将元素添加进去
      • 假如是在index的位置发生哈希冲突,那么通常有一下几种探测方式
        • 线性探测法(线性探测再散列):向后依次探测index+1,index+2…位置,看是否冲突,直到不冲突为止,将元素添加进去。
        • 平方探测法:不探测index的后一个位置,而是探测 2 i 2^i 2i位置上时发生冲突,比如探测 2 0 2^0 20位置上的冲突,接着探测 2 1 2^1 21位置,依此类推,直至冲突解决
    • 链地址法
      • 链表法就是在发生冲突的地址处,挂一个单向链表,然后所有在该位置冲突的数据,都插入这个链表中。插入数据的方式有多种,可以从链表的尾部向头部依次插入数据,也可以从头部向尾部依次插入数据,也可以依据某种规则在链表的中间插入数据,总之保证链表中的数据的有序性。Java的HashMap类就是采取链表法的处理方案
    • 再哈希法
      • 在发生哈希冲突后,使用另外一个哈希算法产生一个新的地址,直到不发生冲突为止。这个应该很好理解
      • 再哈希法可以有效的避免堆积现象,但是缺点是不能增加了计算时间和哈希算法的数量,而且不能保证在哈希表未满的情况下,总能找到不冲突的地址
    • 建立一个公共溢出区
      • 建立一个基本表,基本表的大小等于哈希表的大小建立一个溢出表,所有哈希地址的第一个记录都存在基本表中,所有发生冲突的数据,不管哈希算法得到的地址是什么,都放入溢出表中
      • 但是有一个缺点就是,必须事先知道哈希表的可能大小,而且溢出表里的数据不能太多,否则影响溢出表的查询效率。实际上就是要尽量减少冲突
  • MD5加密算法

    • MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆;所以要解密MD5没有现成的算法,只能用穷举法,把可能出现的明文,用MD5算法散列之后,把得到的散列值和原始的数据形成一个一对一的映射表,通过比在表中比破解密码的MD5算法散列值,通过匹配从映射表中找出破解密码所对应的原始明文
  • 一致性哈希

    • 详见1

    • 详见2

    • 概念

      • 一致性Hash是一种特殊的Hash算法,由于其均衡性、持久性的映射特点,被广泛的应用于负载均衡领域和分布式存储,如nginx和memcached都采用了一致性Hash来作为集群负载均衡的方案
      • 普通的Hash函数最大的作用是散列,或者说是将一系列在形式上具有相似性质的数据,打散成随机的、均匀分布的数据。不难发现,这样的Hash只要集群的数量N发生变化,之前的所有Hash映射就会全部失效。如果集群中的每个机器提供的服务没有差别,倒不会产生什么影响,但对于分布式缓存这样的系统而言,映射全部失效就意味着之前的缓存全部失效,后果将会是灾难性的。一致性Hash通过构建环状的Hash空间代替线性Hash空间的方法解决了这个问题
    • 良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面

      • 平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件
      • 单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区
      • 分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性
      • 负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷
      • 平滑性(Smoothness):平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的
    • 原理

      • 一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H H H的值空间为 0 ∼ 2 32 − 1 0 \sim 2^{32}-1 02321(即哈希值是一个32位无符号整形),整个哈希空间环如下:整个空间按顺时针方向组织,0和 2 32 − 1 2^{32}-1 2321在零点中方向重合

        dhs
      • 下一步将各个服务器使用Hash进行一次哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下

        dhs_align

        接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器

      • 例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下。根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上

        dhs_align_
    • 一致性哈希算法的容错性和可扩展性

      • 现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响

      • 如果在系统中增加一台服务器Node X,如下图所示

        dhs_align_2

        此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响

红黑树

  • 定义及性质

    • 一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
      • 每个结点要么是红的,要么是黑的
      • 根结点是黑的
      • 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的
      • 如果一个结点是红的,那么它的俩个儿子都是黑的
      • 对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点
  • 红黑树的各种操作的时间复杂度都是 O ( log ⁡ n ) \textbf{O}(\log n) O(logn)

  • 红黑树相比于BST和AVL树有什么优点?

    • 红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以 O ( log ⁡ n ) \textbf{O}(\log n) O(logn)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案
    • 相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证 O ( log ⁡ n ) \textbf{O}(\log n) O(logn)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到 O ( n ) \textbf{O}(n) O(n)
    • 红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是 O ( log ⁡ n ) \textbf{O}(\log n) O(logn),所以红黑树应用还是高于AVL树的。实际上插入 AVL 树和红黑树的速度取决于你所插入的数据。如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的
  • 红黑树相对于哈希表,在选择使用的时候有什么依据?

    • 权衡三个因素: 查找速度, 数据量, 内存使用,可扩展性

    • 总体来说,hash查找速度会比map快,而且查找速度基本和数据量大小无关,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n) 小,hash还有hash函数的耗时。如果考虑效率,特别是在元素达到一定数量级时,考虑使用hash。但若你对内存使用特别严格, 希望程序尽可能少消耗内存,那么一定要小心,hash可能会让你陷入尴尬,特别是当你的hash对象特别多时,你就更无法控制了,而且 hash的构造速度较慢

    • 红黑树并不适应所有应用树的领域。如果数据基本上是静态的,那么让他们待在他们能够插入,并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,例如,做一个哈希表,性能可能会更好一些

    • 红黑树天生有序,而对哈希表排序比较麻烦,涉及范围查找,或者排序,红黑树更优

    • 在实际的系统中,例如,需要使用动态规则的防火墙系统,使用红黑树而不是散列表被实践证明具有更好的伸缩性。Linux内核在管理vm_area_struct时就是采用了红黑树来维护内存块的

    • 红黑树通过扩展节点域可以在不改变时间复杂度的情况下得到结点的秩

错题笔记

数学知识

  • 字符串解码

    给定一个经过编码的字符串,返回它解码后的字符串。编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入

    • 解题思路:

      • 本题难点在于括号内嵌套括号,需要从内向外生成与拼接字符串,这与栈的先入后出特性对应
      • 构建辅助栈 stk, 遍历字符串 s 中每个字符 c
        • c 为数字时,将数字字符转化为数字 num,用于后续倍数计算
        • c 为字母时,在 ret 尾部添加 c
        • c[ 时,将当前 numret 入栈,并分别置空置0,用于重新记录
          • 记录此[ 前的临时结果 ret 至栈,用于发现对应 ] 后的拼接操作;
          • 记录此 [前的倍数 num至栈,用于发现对应 ] 后,获取 num× […] 字符串
          • 进入到新 [ 后,retnum 重新记录
        • c]时,stk 出栈,拼接字符串 ret = last_ret + num* ret,其中:
        • last_ret 是上个 [ 到当前 [ 的字符串,例如 “3[a2[c]]” 中的 a;
        • num是当前 [] 内字符串的重复倍数,例如 “3[a2[c]]” 中的 2
        • 返回字符串 ret
    • 代码实现

      string decodeString(string s) {
          int n = s.size();
          string ret, next;
          stack<pair<int, string> > stk;
          int num = 0;
          for(int i = 0; i < n; i++){
              if(s[i]<='9' && s[i]>='0'){
                  num = num*10 + s[i]-'0';
              }
              else if(s[i] == '['){
                  stk.push(std::move(pair<int, string>{num, ret}));
                  num = 0;	// 置零置空
                  ret = "";
              }
              else if(s[i] == ']'){
                  auto p = stk.top();
                  stk.pop();
                  while(p.first--){
                      p.second = p.second + ret;
                  }
                  ret = std::move(p.second);
              }
              else{
                  ret += s[i];
              }
          }
          return ret;
      }
      

单调栈

移掉 K 位数字

给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小

注意:

num 的长度小于 10002 且 ≥ k。
num 不会包含任何前导零。

示例 1 :

输入: num = “1432219”, k = 3
输出: “1219”
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219

  • 解题思路:

    • 以题目中的 num = 1432219, k = 3 为例,我们需要返回一个长度为 4 的字符串,问题在于: 我们怎么才能求出这四个位置依次是什么呢?
    • 暴力法的话,我们需要枚举C_n^(n - k) 种序列(其中 n 为数字长度),并逐个比较最大。这个时间复杂度是指数级别的,必须进行优化
    • 一个思路是利用数学知识
      • 对于两个数 123a456123b456,如果 a > b, 那么数字 123a456 大于 数字 123b456,否则数字 123a456 小于等于数字 123b456。也就说,两个相同位数的数字大小关系取决于第一个不同的数的大小
      • 从左到右遍历
      • 对于每一个遍历到的元素,决定是丢弃还是保留
      • 遍历过程中,如果左边数字比当前数字小,则不能丢弃左边数字,否则导致数字变大
    • 1432219为例,于是得到解题流程为
      • 当前数字为1,左边没有元素,保留
      • 当前数字为4,左边为1,保留左边元素
      • 以此类推
    • 然而需要注意的是,如果给定的数字是一个单调递增的数字,那么我们的算法会永远选择不丢弃。这个题目中要求的,我们要永远确保丢弃 k 个矛盾
    • 一个简单的思路就是:
      • 每次丢弃一次,k 减去 1。当 k 减到 0 ,我们可以提前终止遍历
      • 而当遍历完成,如果 k 仍然大于 0。不妨假设最终还剩下 x 个需要丢弃,那么我们需要选择删除末尾 x 个元素
      • 刚才我们的关注点一直是丢弃,题目要求我们丢弃 k 个。反过来说,就是让我们保留 n - k个元素,其中 n 为数字长度。 那么我们只需要按照上面的方法遍历完成之后,再截取前n - k个元素即可
  • 代码实现

    string removeKdigits(string num, int k) {
    	if(k<=0)
        	return num;
        string ret;
        for(auto & ch : num){
            while(k > 0 && !ret.empty() && ch < ret.back()){
                ret.pop_back();
                --k;
            }
            ret.push_back(ch);
        }
        while(k-- > 0 && !ret.empty())  // 清除尾部多余数字,并防止k>num.size()的情况
            ret.pop_back();
        int idx = 0;
        while(idx < ret.size() && ret[idx] == '0')  // 清除头部的'0'
            ++idx;
        ret = ret.substr(idx);
        return ret.empty() ? "0" : ret; 
    }
    

数组/双指针

  • 下一个排列

    题目描述:实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。

    • 思路:注意到下一个排列总是比当前排列要大,除非该排列已经是最大的排列。我们希望找到一种方法,能够找到一个大于当前序列的新序列,且变大的幅度尽可能小。具体地:

      • 我们需要将一个左边的「较小数」与一个右边的「较大数」交换,以能够让当前排列变大,从而得到下一个排列

      • 同时我们要让这个「较小数」尽量靠右,而「较大数」尽可能小。当交换完成后,「较大数」右边的数需要按照升序重新排列。这样可以在保证新排列大于原来排列的情况下,使变大的幅度尽可能小

      • 以排列 [4,5,2,6,3,1] 为例:

        • 我们能找到的符合条件的一对「较小数」与「较大数」的组合为 2 与 3,满足「较小数」尽量靠右,而「较大数」尽可能小
        • 当我们完成交换后排列变为 [4,5,3,6,2,1],此时我们可以重排「较小数」右边的序列,序列变为 [4,5,3,1,2,6]
    • 代码实现

      void nextPermutation(vector<int> & nums){
          if(nums.size()<=1)
              return;
          int n = nums.size();
          int smaller = n-2;
          while(smaller>=0 && nums[smaller] >= nums[smaller + 1])
              --smaller;
          if(smaller >= 0){
              int bigger = n - 1;
              while(bigger >= 0 && nums[bigger] <= nums[smaller])
                  --bigger;
              swap(nums[smaller], nums[bigger]);
          }
          reverse(nums.begin() + smaller + 1, nums.end());
      }
      

DFS+回溯

  • 括号生成

    题目描述:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合

    • 解题思路:如果左括号数量不大于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号

    • 代码实现

      vector<string> generateParenthesis(int n) {
          if(n <= 0)
              return {};
          vector<string> ret;
          string tmp = string("");
          dfs(ret, tmp, n, n);
          return ret;
      }
      
      void dfs(vector<string> & ret, string &str, int open, int close)
      {
          if(open ==0 && close ==0)
          {
              ret.push_back(str);
              return;
          }
          if(open > 0)
          {
              str.push_back('(');
              dfs(ret, str, open-1, close);
              str.pop_back();
          }
          if(close > open)
          {
              str.push_back(')');
              dfs(ret, str, open, close - 1);
              str.pop_back();
          }
      }
      
  • 删除无效的括号

    题目描述:给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。返回所有可能的结果。答案可以按 任意顺序 返回

    • 例子

      输入: "()())()"
      输出: ["()()()", "(())()"]
      
    • 思路:

      • 如果当前遍历到的左括号的数目严格小于右括号的数目则表达式无效
      • 因此,我们可以遍历一次输入字符串,统计「左括号」和「右括号」出现的次数
        • 当遍历到「右括号」的时候
        • 如果此时「左括号」的数量不为 0,因为 「右括号」可以与之前遍历到的「左括号」匹配,此时「左括号」出现的次数 -1
        • 如果此时「左括号」的数量为 0,「右括号」数量加 1
        • 当遍历到「左括号」的时候,「左括号」数量加 1
      • 通过这样的计数规则,最后「左括号」和「右括号」的数量就是各自最少应该删除的数量
    • 代码实现

      vector<string> removeInvalidParentheses(string s) {
          if(s.empty())
              return {""};
          set<string> ret;	// hashset去重
          int leftRemove = 0, rightRemove = 0;    // 记录最少删除多少左括号和右括号
          int n = s.size();
      
          for(auto & ch: s){
              if(ch == '(')
                  leftRemove++;
              else if(ch == ')'){
              	if(leftRemove == 0) // 多余右括号
                  	rightRemove++;
                  if(leftRemove > 0)  // 可以抵消一个左括号
                      leftRemove--;
              }
          }
          string path;
          dfs(ret, s, path, 0, 0, 0, leftRemove, rightRemove);
          return vector<string> (ret.begin(), ret.end());
      }
      
      void dfs(set<string> & ret, string &s, string &path, int idx, int left, int right, int leftRemove, int rightRemove){
          if(idx == s.size()){
              if(leftRemove == 0 && rightRemove == 0){
                  ret.insert(path);
              }
              return;
          }
              
          char ch = s[idx];
          // 删除当前字符
          if(ch == '(' && leftRemove > 0)
              dfs(ret, s, path, idx+1, left, right, leftRemove-1, rightRemove);
          else if(ch == ')' && rightRemove > 0)
              dfs(ret, s, path, idx + 1, left, right, leftRemove, rightRemove-1);
      
          // 保留当前字符
          path.push_back(ch);
          if(ch != '(' && ch!=')')    // 字母直接保留
              dfs(ret, s, path, idx+1, left, right, leftRemove, rightRemove);
          else if(ch == '(')      // 保留左括号,left+1
              dfs(ret, s, path, idx+1, left+1, right, leftRemove, rightRemove);
          else if(left > right)     // 左括号数大于右括号数时才能保留右括号,right+1
              dfs(ret, s, path, idx+1, left, right + 1, leftRemove, rightRemove);
      
          path.pop_back();	// 回溯
      
      }
      

二分查找

  • 寻找两个正序数组的中位数

    题目描述:给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

    • 代码实现

      int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
          /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
           * 这里的 "/" 表示整除
           * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
           * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
           * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
           * 这样 pivot 本身最大也只能是第 k-1 小的元素
           * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
           * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
           * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
           */
      
          int m = nums1.size();
          int n = nums2.size();
          int index1 = 0, index2 = 0;
      
          while (true) {
                  // 边界情况
              if (index1 == m) {
                  return nums2[index2 + k - 1];
              }
              if (index2 == n) {
                  return nums1[index1 + k - 1];
              }
              if (k == 1) {
                  return min(nums1[index1], nums2[index2]);
              }
      
              // 正常情况
              int newIndex1 = min(index1 + k / 2 - 1, m - 1);
              int newIndex2 = min(index2 + k / 2 - 1, n - 1);
              int pivot1 = nums1[newIndex1];
              int pivot2 = nums2[newIndex2];
              if (pivot1 <= pivot2) {
                  k -= newIndex1 - index1 + 1;
                  index1 = newIndex1 + 1;
              }
              else {
                  k -= newIndex2 - index2 + 1;
                  index2 = newIndex2 + 1;
              }
          }
      }
      
      double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
          int totalLength = nums1.size() + nums2.size();
          if (totalLength % 2 == 1) {
              return getKthElement(nums1, nums2, (totalLength + 1) / 2);
          }
          else {
              return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
          }
      }
      

滑动窗口

  • 最小覆盖子串

    • 给出两个字符串 S S S T T T,要求在 O ( n ) \textbf{O}(n) O(n)的时间复杂度内在中 S S S找出最短的包含 T T T中所有字符的子串。
      例如:

      S = " A D O B E C O D E B A N C " S="ADOBECODEBANC" S="ADOBECODEBANC"
      T = " A B C " T="ABC" T="ABC"
      找出的最短子串为 B A N C BANC BANC

      注意:
      如果 S S S中没有包含 T T T中所有字符的子串,返回空字符串 “”;
      满足条件的子串可能有很多,但是题目保证满足条件的最短的子串唯一

    • 思路:可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的right指针,和一个用于「收缩」窗口的left指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 S S S上滑动窗口,通过移动right指针不断扩张窗口。当窗口包含T全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口

    • 优化:其实在 S S S 中,有的字符我们是不关心的,我们只关心 T T T中出现的字符,我们可以先预处理 S S S,扔掉那些 T T T中没有出现的字符,然后再做滑动窗口

    • 代码

      	string minWindow(string S, string T) {
              // write code here
              unordered_map<char, int> window, target;
              int left = 0, right =0, n=S.size();
              int start = 0, count = 0;
              int minLen = INT_MAX;
              for(auto & ch : T)
                  ++target[ch];
              while(right<n){
                  char ch = S[right];
                  ++right;
                  if(target.count(ch)>0){  // 优化,仅统计T中出现的字符
                      ++window[ch];
                      if(window[ch] == target[ch])
                          ++count;
                      while(count == int(target.size())){
                          if(right-left < minLen){
                              start = left;
                              minLen = right-left;
                          }
                          ch = S[left];
                          ++left;		// 右移缩小窗口
                          if(target.count(ch) > 0){	// 当前left所指元素在T中,减1
                              if(window[ch] == target[ch]) 
                                  --count;
                              --window[ch];
                          }
                      }
                  }
              }
              return minLen == INT_MAX?"":S.substr(start, minLen);
          }
      

动态规划

  • 分苹果:把m个同样的苹果放在n个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法

    • 思路: d p [ n ] [ m ] dp[n][m] dp[n][m] n n n个盘子 m m m个苹果总摆放数, d p [ i ] [ j ] dp[i][j] dp[i][j] i i i个盘子 j j j个苹果的摆放数,则有

      • 如果 i > j i>j i>j则,必有一个以上是空盘,其摆放方式相当于将 m m m个苹果放入 i − 1 i-1 i1个盘子,所以 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]
      • 如果 i ≤ j i\le j ij则,有两种情况:
        • 如果有一个以上空盘, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]
        • 若每个盘子都摆有一个苹果,摆放数为相当于将剩余 j − i j-i ji个苹果摆放到 i i i个盘子中
        • 摆放总数为上面两种情况的和: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − i ] dp[i][j] = dp[i-1][j] + dp[i][j-i] dp[i][j]=dp[i1][j]+dp[i][ji]
      • 边界条件,只有一个苹果或0个苹果时,摆放数为1
    • 代码

      int putApples(int n, int m){
          if(n < 0||m < 0)
              return 0;
          vector<vector<int> > dp(n+1, vector<int> (m+1, 0));
          
          for(int i = 0; i <= n; i++)
              dp[i][0] = dp[i][1] = 1;
          
          for(int i = 1; i <= n; i++){
              for(int j = 2; j <= m; j++){
                  dp[i][j] = dp[i-1][j];
                  if(j >= i)
                      dp[i][j] += dp[i][j-i];
              }
          }
          return dp[n][m];
      }
      
  • 最长递增子序列:最长递增子序列不唯一,但是最长大小唯一,可以使用动态规划

    • 代码

      int LIS(vector<int> & arr){
          if(arr.empty())
              return 0;
          int n = arr.size();
          vector<int> dp(n, 1);
          int maxLen = 0;
          for(int i = 1; i < n; i++){
              for(int j = 0; j < i; j++){
                  if(arr[j] < arr[i]){
                      dp[i] = max(dp[i], dp[j] + 1);
                  }
              }
              maxLen = max(maxLen, dp[i]);
          }
          return maxLen;
      }
      
  • 给定数组arr,设长度为n,输出arr的最长递增子序列。如果有多个答案,请输出其中字典序最小的。此时需要输出字典序中最小的子序列

    • 解法:

      • 两步走:1. 求最大递增子序列长度;2. 求字典靠前子序列
    • 第一步思路:

      • 贪心+二分:假设数组arr为[2, 3, 1, 2, 3]vec数组里面存放递增子序列,maxLen数组里存放以元素i结尾的最大递增子序列长度,那么遍历数组arr并执行如下更新规则:
      1. 初始情况下,vec为[2],maxLen[1]
      2. 接下来遇到3,由于vec最后一个元素小于3,直接更新,vec为[2,3],maxLen[1,2]
      3. 接下来遇到1,由于vec最后的元素大于1, 我们在vec中查找大于等于1的第一个元素的下标,并用1替换之,此时vec为[1,3], maxLen[1,2,1]
      4. 接下来遇到2,由于vec最后的元素大于2,我们在vec中查找大于等于2的第一个元素的下标,并用2替换之,此时vec为[1,2], maxLen[1,2,1,2]
      5. 接下来遇到3,由于vec最后一个元素小于3,直接更新,vec为[1,2,3],maxLen为[1,2,1,2,3]
      6. 此时vec的大小就是整个序列中最长递增子序列的长度(但是vec不一定是本题的最终解)
    • 第二步思路:

      • 假设我们原始数组是arr,得到的maxLen为[1,2,3,1,3],最终输出结果为res(字典序最小的最长递增子序列),res的最后一个元素在arr中位置无庸置疑是maxLen[i]==3对应的下标,那么到底是arr1[2]还是arr[4]呢?如果是arr[2],那么arr[2]<arr[4],则maxLen[4]=4,与已知条件相悖。因此我们应该取arr[4]放在res的最后一个位置
    • 代码实现

      vector<int> LIS(vector<int> & arr){
          if(arr.empty())
              return {};
          vector<int> ret;
          vector<int> maxLen;
          int n = arr.size();
          ret.emplace_back(arr[0]);
          maxLen.emplace_back(1);
          for(int i = 1; i < n; ++i){
              if(arr[i] > ret.back()){
                  ret.emplace_back(arr[i]);
                  maxLen.emplace_back(ret.size());
              }
              else{
                  int pos = lower_bound(ret.begin(), ret.end(), arr[i]) - ret.begin();	// 第一个大于等于arr[i]的位置            			arr[pos] = arr[i];
                  maxLen.emplace_back(pos + 1);
              }   
          }
          for(int i = n - 1, j = ret.size(); i>=0; --i){	// 从后往前填充
              if(maxLen[i] == j)
                  ret[--j] = arr[i];
          }
          return ret;
      }
      
  • 正则表达式匹配:给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

    ‘.’ 匹配任意单个字符
    ‘*’ 匹配零个或多个前面的那一个元素
    所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串

    • 思路一:用 f [ i ] [ j ] f[i][j] f[i][j]表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。考虑 p 的第 j 个字符的匹配情况

      • 如果p的第 j 个字符是一个小写字母,那么我们必须在 s 中匹配一个相同的小写字母,即

        f [ i ] [ j ] = { f [ i − 1 ] [ j − 1 ] , s [ i ] = p [ j ] ; f a l s e , s [ i ] ≠ p [ j ] . f[i][j] = \begin{cases}&f[i-1][j-1], &s[i]=p[j];\\ &false, &s[i] \ne p[j]. \end{cases} f[i][j]={f[i1][j1],false,s[i]=p[j];s[i]=p[j].

      • 字母 + 星号的组合在匹配的过程中,本质上只会有两种情况

        • 匹配 s 末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配;
        • 不匹配字符,将该组合扔掉,不再进行匹配
      • 可以得到字母 + 星号的组合的状态转移方程为

        f [ i ] [ j ] = { f [ i − 1 ] [ j ]    o r    f [ i ] [ j − 2 ] , s [ i ] = p [ j − 1 ] ; f [ i ] [ j − 2 ] , s [ i ] ≠ p [ j − 1 ] . f[i][j] = \begin{cases}&f[i-1][j] \; or\; f[i][j-2], &s[i]=p[j-1];\\ &f[i][j-2], &s[i] \ne p[j-1]. \end{cases} f[i][j]={f[i1][j]orf[i][j2],f[i][j2],s[i]=p[j1];s[i]=p[j1].

      • 在任意情况下,只要p[j] 是 .,那么p[j] 一定成功匹配 s 中的任意一个小写字母

      • 边界条件:两个空字符串是可以匹配的,$f[0][0] = $true

    • 思路二:递归实现

      regex_recursive
    • 代码实现

      bool isMatch(string s, string p) {
          int m = s.size();
          int n = p.size();
      
          auto matches = [&](int i, int j) {
              if (i == 0) {
                  return false;
              }
              if (p[j - 1] == '.') {
                  return true;
              }
              return s[i - 1] == p[j - 1];
          };
      
          vector<vector<bool>> f(m + 1, vector<bool>(n + 1, false));
          f[0][0] = true;
          for (int i = 0; i <= m; ++i) {
              for (int j = 1; j <= n; ++j) {
                  if (p[j - 1] == '*') {
                      f[i][j] = f[i][j] | f[i][j - 2];	// 不匹配字母+星号
                      if (matches(i, j - 1)) {		// 星号前字母匹配
                          f[i][j] = f[i][j] | f[i - 1][j];  
                      }
                  }
                  else {
                      if (matches(i, j)) {
                          f[i][j] = f[i][j] | f[i - 1][j - 1];
                      }
                  }
              }
          }
          return f[m][n];
      }
      
      // 递归实现
      bool isMatch(string s, string p) {
          if(s.empty() && p.empty())
              return true;
          if(p.empty())
              return false;
          int m = s.size(), n = p.size();
          if(n > 1 && p[1]=='*'){		// alpha + *
              if(m > 0 && (s[0] == p[0] || p[0]=='.')){	// 星号前字母匹配
                  return isMatch(s.substr(1), p) 			 // 匹配大于等于一个alpha
                      || isMatch(s, p.substr(2));			// 不匹配alpha,丢弃 alpha + *
              }
              else{
                  return isMatch(s, p.substr(2));  // 星号前字母不匹配,丢弃 alpha + *
              }
          }
          if(m > 0 && (s[0] == p[0] || p[0] == '.'))
              return isMatch(s.substr(1), p.substr(1));  
          return false;
      }
      
      

匈牙利算法

  • 匈牙利算法主要用于解决一些与二分图匹配有关的问题

  • 二分图Bipartite graph)是一类特殊的,它可以被划分为两个部分,每个部分内的点互不相连。下图是典型的二分图

    binary_graph
  • 可以看到,在上面的二分图中,每条边的端点都分别处于点集X和Y中。匈牙利算法主要用来解决两个问题:求二分图的最大匹配数最小点覆盖数

  • 例题:若两个正整数的和为素数,则这两个正整数称之为“素数伴侣”,如2和5、6和13,它们能应用于通信加密。现在密码学会请你设计一个程序,从已有的N(N为偶数)个正整数中挑选出若干对组成“素数伴侣”,挑选方案多种多样,例如有4个正整数:2,5,6,13,如果将5和6分为一组中只能得到一组“素数伴侣”,而将2和5、6和13编组将得到两组“素数伴侣”,能组成“素数伴侣”最多的方案称为“最佳方案”,当然密码学会希望你寻找出“最佳方案”。

  • 素数(质数):指只能被1和本身整除的数,判断方法:

    bool isPrime(int num){
        if(num<=1)
            return false;
        for(int i = 2; i*i <= num; i++)
            if(num%i==0)
                return false;
        return true;
    }
    
  • 解题思路:把输入的数分为奇数和偶数两个集合,因为两个奇数或两个偶数的和为偶数,必能被2整除,不为素数。所以问题转变成,求奇数集合和偶数集合构成二分图,通过判断奇数和偶数两两相加是否为素数构建邻接矩阵,然后利用匈牙利算法求最大匹配数

  • 代码:

    // 寻找oddIdx下标指向的奇数的匹配
    bool match(vector<vector<bool> > & map, vector<int> & record, vector<bool> & visited, int oddIdx){
        for(int i = 0; i<record.size(); i++){
            if(map[oddIdx][i] && !visited[i]){		// odds[oddIdx]和evens[i]之间有路径,并且evens[i]未检查
                visited[i] = true;			// 记录evens[i]已检查
                if(record[i]==-1|| match(map, record, visited, record[i])){		// evens[i]未匹配或者已批配,并递归
                    record[i] = oddIdx;											// 寻找能否将evens[i]已匹配的奇数,找到
                    return true;												// 另一个偶数匹配,让出evens[i]给odds[oddIdx]
                }
            }
        }
        return false;
    }
    
    int main(){
        int n;
        while(cin>>n){
            vector<int> odds;
            vector<int> evens;
            int tmp;
            for(int i = 0; i<n;i++){
                cin>>tmp;
                if(tmp&1)
                    odds.push_back(tmp);
                else
                    evens.push_back(tmp);
            }
            vector<vector<bool> > map(odds.size(), vector<bool> (evens.size(), false));	// 邻接矩阵
            for(int i = 0; i < odds.size(); i++){
                for(int j = 0; j<evens.size(); j++){
                    if(isPrime(odds[i] + evens[j]))		// 构建邻接矩阵
                        map[i][j] = true;
                }
            }
            int count = 0;
            vector<int> record(evens.size(), -1);	// 记录evens[i]与odds中哪个数匹配
            for(int i = 0; i < odds.size();i++){
                vector<bool> visited(n, false);		// 记录i下表所指奇数对应偶数匹配情况
                if(match(map, record, visited, i))	// 能找到当前奇数的匹配,count+1
                    count++;
            }
            
            cout<<count<<endl;
        }
        return 0;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值