前言
以前在学校只专注于研究感兴趣的技术,开发一些乱七八糟的项目,几乎没刷过题,现在面试笔试被各种毒打。现在开始亡羊补牢。只记录算法或思路,不记录题目,不间断更新,顺便重拾C++
排序算法
冒泡排序
void BubbleSort(int arr[], int len)
{
for (int i = 0; i < len; i++) {
//因为第len - i后面的元素是已经排好了,所以每次只需要遍历 0 到 len - 1 - i 个元素
for (int j = 0; j < len - 1 - i; j++) {
//若当前元素大于下一个元素,则交换
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
选择排序
void SelectSort(int arr[], int len)
{
for (int i = 0; i < len; i++) {
int minIndex = i;
//从元素下一个开始往后找最小的
for (int j = i + 1; j < len; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
//将当前下标的元素与最小元素交换
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
插入排序
void InsertSort(int arr[], int len)
{
//第一个不用排,从1开始
for (int i = 1; i < len; i++) {
int cur = arr[i];//先拿出要进行插入的元素
int insertIndex = 0;//下面循环中如果一直找不到可以插的位置,直到循环结束,说明目前它最小,就插到第一个元素的位置
//往前找有没有还可以插入的地方,同时把排好的元素向后移动
for (int j = i - 1; j >= 0; j--) {
if (cur < arr[j]) {
arr[j + 1] = arr[j];//当前选择的元素比排好的小,排好的元素后移
} else {
insertIndex = j + 1;//插入位置为当前与cur比较的元素的前面
break;
}
}
arr[insertIndex] = cur;
}
}
希尔排序
//希尔排序 使数据逐渐变得有序,最后在比较有序的数据里进行插入排序,减少插入排序时移动元素的次数
void ShellSort(int arr[],int len)
{
int gap = len / 2;
while (gap > 0)
{
//外面一层循环次数为数组的组数,里面一层为每组的元素个数
for (int i = gap; i < len; i++) {
int cur = arr[i];//先拿出要进行插入的元素
int insertIndex = i - gap;//下面循环中如果一直找不到可以插的位置,直到循环结束,说明目前它最小,就插到第一个元素的位置
//往前找有没有还可以插入的地方,同时把排好的元素向后移动,
//不过希尔排序需要跨步,把一个数组看成若干个交错在一起的数组,gap就是跨的距离,直到gap = 1,就变成普通插入排序
for (int j = i - gap; j >= 0; j -= gap) {
if (cur < arr[j]) {
arr[j + gap] = arr[j];//当前选择的元素比排好的小,排好的元素后移
} else {
insertIndex = j + gap;//插入位置为当前与cur比较的元素的前面
break;
}
}
arr[insertIndex] = cur;
}
gap /= 2;
}
}
归并排序
//将数组start到mid 和 mid+1到end进行合并
void MergeArr(int arr[], int start, int mid, int end, int temArr[])
{
int i = start, j = mid + 1, k = 0;
//比较长度相同部分
while (i <= mid && j <= end) {
if (arr[i] < arr[j])
temArr[k++] = arr[i++];
else
temArr[k++] = arr[j++];
}
//i超出部分
while (i <= mid) {
temArr[k++] = arr[i++];
}
//j超出部分
while (j <= end) {
temArr[k++] = arr[j++];
}
for (int i = 0; i < k; i++)
arr[start + i] = temArr[i];
}
void RecursionMergeSort(int arr[], int start, int end, int temArr[])
{
if (start >= end) return;//结束递归的条件
//排序两边的数组
int mid = (start + end) / 2;
RecursionMergeSort(arr, start, mid, temArr);
RecursionMergeSort(arr, mid + 1, end, temArr);
//将排好的数组合并
MergeArr(arr, start, mid, end, temArr);
}
//归并排序
void MergeSort(int arr[],int len)
{
int* temArr = (int*)malloc(len * sizeof(int));
RecursionMergeSort(arr, 0, len - 1, temArr);
}
快速排序
void Swap(int arr[], int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//快速排序 前后指针法
void QuckSort(int arr[], int start, int end)
{
if (start >= end) return; //递归结束条件
//取第一个元素为基准
int key = arr[start];
int prev = start;
int latter = prev + 1;
while (latter <= end) {
//若后面的值比key小,交换到前面去,不要交换第一个位置,交换完成后latter后移
if (arr[latter] < key) {
Swap(arr, ++prev, latter);
}
latter++;
}
//当latter越界后prev所在的位置就是数列中比key小的最后那个数,交换它们
Swap(arr, start, prev);
//继续排序左右两边 prev所指的key不用继续排列了
QuckSort(arr, start, prev - 1);
QuckSort(arr, prev + 1, end);
}
动态规划
个人理解动态规划就是把一个大的问题分解成逐个小问题,把每个小问题的计算结果记录起来,后续的计算可以引用之前已经计算好的部分,避免大量的重复计算。
例如:
最长上升子序列
思路:
例如从[6,3,1,5,2,3,7]找最长上升子序列长度,先分解成小问题,用一个数组bp来记录每个小问题的结果:
[6]:由于只有一个元素,所以长度就是1,序列为[6],bp[0]=1。
[6,3]:已知[6]的最长上升子序列是1,3需要往前找有没有比它小的元素,有的话最长上升子序列长度=前面所有比它小的元素里,最长上升子序列最大的长度+1,没有的话就是它自己而已那就是1,很显然它前面只有6,3不大于6,所以[6,3]最长上升子序列长度为1,bp[1]=1。
[6,3,1]:同上,还是1,bp[2]=1。
[6,3,1,5]:5的前面有比它小的1和3,取[1,5]时,bp[3]=bp[2]+1;取[3,5]时,bp[3]=bp[1]+1;对比一下[1,5]和[3,5],找最长的,发现一样长,所以取其一即可,序列为[3,5]或者[1,5],bp[3]=2。
[6,3,1,5,2]:2前面比它小的只有1,[6,3,1]最大长度为1,bp[4]=bp[2]+1=2,序列为[1,2]。
[6,3,1,5,2,3]:3前面有1和2可取,[6,3,1]最大长度是1,[6,3,1,5,2]最大长度是2,取2比取1大,所以bp[5]=bp[4]+1=3,序列为[1,2,3]。
[6,3,1,5,2,3,7]:7前面的数都比它小,找最大的,最大是bp[5]=3,所以bp[6]=bp[5]+1=4,所以数组[6,3,1,5,2,3,7]的最长上升子序列为[1,2,3,7],长度为4。
可以看到这里使用数组bp来记录之前的计算结果,用到的时候去查一下即可,避免了重复运算。例如计算[6,3,1,5,2,3,7]的最长上升子序列长度时,需要知道[6,3,1,5,2,3]里有没有比7小的元素,如果有,那么这个元素当前的最长上升子序列长度是多少。如果没有bp进行记录,又需要一个个去计算[6,3,1,5,2,3]每个比7小的元素为结尾的最长上升子序列长度。
public int LIS(List<int> arr)
{
if (arr.Count == 0) return 0;
int max = 0;
int[] bp = new int[arr.Count];
bp[0] = 1;//当i=0,长度是1
for (int i = 1; i < arr.Count; i++)
{
//初始化最小长度是1,不能是0
if (bp[i - 1] == 0) bp[i - 1] = 1;
for(int j = i - 1; j >= 0; j--)
{
//在后面比当前元素小的元素里,查找最大的上升子序列长度
if (arr[i] > arr[j] && bp[j] + 1 > bp[i])
{
bp[i] = bp[j] + 1;
max = bp[i] > max ? bp[i] : max;
}
}
}
return max;
}
一些题型解题思路
统计素数(或者叫质数)个数
//暴力方式
/*
* 3*6 和 6*3是一样的,乘法交换律
* 判断了3*6就没必要再去判断6*3,所以循环次数为sqrt(n),也可表示为i*i<n
* 是偶数也没必要判断
*/
bool isPrimer(int n)
{
//i * i <= n不能是i * i < n,如果n能被i整除也不是素数,例如9,如果仅仅是小于号就判断为素数了
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}
int PrimerCount(int n)
{
if (n < 2) return 0;
if (n == 2) return 1;
int count = 1;//2算一个
for (int i = 3; i <= n; i+=2) {
if (isPrimer(i)) count++;
}
return count;
}
//埃筛法
/*
* 每次找到素数后都去把它的倍数标记起来,能被它整除的都不是素数了
* 每个合数(不是素数就叫合数)都有素数因子
*/
int PrimerCountAS(int n)
{
if (n < 2) return 0;
int count = 0;
bool *isPrimer = new bool[n + 1];
memset(isPrimer, true, n * sizeof(bool));
for (int i = 2; i <= n; i++) {
if (isPrimer[i]) {
count++;
//查找i的倍数并标记不是素数
for (int j = i + i; j <= n; j += i) {
isPrimer[j] = false;
}
}
}
delete[] isPrimer;
return count;
}
数组中3数最大乘积
同符号直接取最大前3个数相乘,有正有负则保证结果为正的前提下,取绝对值最大的3个
判断链表是否有环
双指针算法,慢指针每次位移一个位置,快指针比慢指针快一步,每次位移两个位置。
如果进入到环,因为指针比较快会迟早从后面追上慢指针。
当快指针==慢指针,代表有环,但快指针为null或它的下一个为null,则遍历结束,没有环。
另一个方法:
每次遍历到一个节点的时候,给它标上一个下标,当发现下一个节点被标记过,则有环
求最长为n的数组能构成的二叉搜索树的个数
其实就是求卡塔兰数,它的递推公式: