跳转至

算法题

1 算法思想

1.1 动态规划

  • 算法思想: 动态规范把原问题分解成许多子问题,自底向上求解子问题,合并子问题的解。子问题解保存在数组中,供后续子问题使用,避免重复计算,增加算法效率。
  • 动态规范要素:
  • 最优子结构
  • 重叠子问题
title: tip 重叠子问题
在求解子问题过程中,有大量子问题是重复的,只需将求得的子问题解保存在数组中,以后使用时直接调用,增加了算法效率。重叠子问题不是动态规划的必要条件,但更能体现动态规划优点。

2 排序算法

2.1 冒泡排序

  1. 从后往前排
  2. 相邻2数比较、交换
  3. 2层for循环
  4. 时间复杂度$O(n^{2})$
  5. 空间复杂度$O(1)$
  6. 示例
/**
 * @brief 冒泡排序
 * 相邻元素比较,交换,每一次循环最值都在数组顶部
 * @tparam T 
 * @param data 
 * @param num 
 */
template <typename T>
void sort(T data[], int num)
{
    for (size_t i = 0; i < num; ++i)
    {
        for (size_t j = num; j > i; --j)
        {
            if (data[j] < data[j - 1])
            {
                T tmp = data[j];
                data[j] = data[j - 1];
                data[j - 1] = tmp;
            }
        }
    }
}

2.2 选择排序

  1. 每第i次循环从n-i元素中选择最小元素k与第i元素交换
  2. 2层for循环
  3. 时间复杂度$O(n^{2})$
  4. 空间复杂度$O(1)$
  5. 示例
/**
 * @brief 选择排序
 * 从第i~n范围元素中找出最小值元素索引min,交换data[i]与data[min]数据 
 * @tparam T 
 * @param data 
 * @param num 
 */
template <typename T>
void sort(T data[], int num)
{
    for (size_t i = 0; i < num - 1; ++i)
    {
        //最小元素索引
        int min = i;
        for (size_t j = i + 1; j < num; ++j)
        {
            if (data[j] < data[min])
                min = j;
        }
        if (i != min)
        {
            T tmp = data[min];
            data[min] = data[i];
            data[i] = tmp;
        }
    }
}

2.3 插入排序

  1. 将第i(i>=2)与前i-1个元素依次比较,插入到前i-1个元素中
  2. 2层for循环
  3. 时间复杂度$O(n^{2})$
  4. 空间复杂度$O(1)$
  5. 示例
/**
 * @brief 插入排序
 * 将第i个元素与前i-1个元素比较,找到合适位置j,j~i-1间元素向后移动以为,将data[i]插入data[j]处
 * @tparam T 
 * @param data 
 * @param num 
 */
template <typename T>
void sort(T data[], int num)
{
    for (int i = 1; i < num; ++i)
    {
        int j;
        T tmp = data[i];
        for (j = i; j > 0; --j)
        {
            if (tmp < data[j - 1]) //将大值往后一位
                data[j] = data[j - 1];
            else //找到合适位置
                break;
        }
        //j位置就是合适位置
        data[j] = tmp;
    }
}

2.4 快速排序

  1. 首尾指针,选择首元素作为基本值(一般选择首尾或中间值)
  2. 先比较尾部元素与基本值大小,小于基本值与头指针指向元素交换,再移动头指针元素比较直到元素值大于基本值,再与尾部指针元素交换,再移动尾部指针,循环此操作,直到头尾指针相等。
  3. 再与头尾指针汇聚处作上述处理,直到分组中只含一个元素,此时元素以排序。
  4. 平均时间复杂度$O(n\log_{}{n})$
  5. 最差时间复杂度$O(n^2)$

2.5 归并排序

  1. 递归将数组分成2部分
  2. 将两两子数组合并并排序组成一个已排序子数组
  3. 最终合并成一个已排序数组
  4. 平均时间复杂度$O(n\log_{}{n})$
  5. 最差时间复杂度$O(n^2)$
  6. 示例
template <typename T>
void mergeSort(T data[], int L, int R);
template <typename T>
void merge(T data[], int L, int R, int M);
/**
 * @brief 归并排序
 * 将数组分成多个子数组,再将子数组合并时排序,最终合并成一个排序数组
 * @tparam T 
 * @param data 
 * @param num 
 */
template <typename T>
void sort(T data[], int num)
{
    mergeSort(data, 0, num - 1);
}

template <typename T>
void mergeSort(T data[], int L, int R)
{
    //递归终止条件
    if (L == R)
        return;
    int mid = (L + R) / 2;
    //分割
    mergeSort(data, L, mid);
    mergeSort(data, mid + 1, R);
    //合并
    merge(data, L, R, mid);
}

template <typename T>
void merge(T data[], int L, int R, int M)
{
    T *arr = new T[R - L + 1];
    int l = L, r = M + 1, index = 0;
    //排序完成条件
    while (l <= M && r <= R)
    {
        if (data[l] <= data[r])
        {
            arr[index++] = data[l++];
        }
        else
        {
            arr[index++] = data[r++];
        }
    }
    //将剩下的元素放入临时数组中
    while(l <= M)
    { //左数组剩余
        arr[index++]=data[l++];
    }
    while (r<=R)
    {//右数组剩余
        arr[index++]=data[r++];
    }
    //复制临时数组中元素到原数组中
    for(index=0;L<=R;++L)
    {
        data[L]=arr[index++];
    }
    //释放临时数组
    delete []arr;
}

2.6 堆排序

  1. 以二叉树形式查找n个元素中最值,将其放入数组末尾
  2. 在n-1次循环处理下,数组已排好序
  3. 平均时间复杂度$O(n\log_{}{n})$
  4. 最差时间复杂度$O(n\log_{}{n})$

3 算法常识

3.1 推导大O阶方法

  1. 用常数1替代所有加法性质的常数
  2. 只保留最高阶项,去除其它次要项
  3. 如果最高项含常量系数(如3n则去除3,只保留n),则去除常量系数

常用时间复杂度排序表

4 英文字符大小写转换

前置知识:A~Z范围是0x41~0x0x5a(65~90),a~z范围是0x61~0x7a(97~122) 1. 传统方法:遍历字符串大写字母+32,小写字母-32 2. 高级方法:因为大小写字母只在第6bit位置上不同,如A(0100 0001)和a(0110 0001),因此采用对0x40(0010 0000)异或运算就能达到大小写字母转换。如01000001^0x0010000结果是01100001,即A。

5 树

  • 度:指节点含多少个子节点
  • 树的深度:指树有多少层
  • 完全二叉树:从左往右按顺序排列节点二叉树
  • 满二叉树:叶子节点都在同一层的完全二叉树

5.1 二叉树

  • 深度为k的二叉树最多有$2^{k}-1(k\ge 1)$个节点
  • 第i层上,最多有$2^{i-1}(i\ge1)$个节点
  • 假设叶子节点树是$n_{0}$,度为2的节点数为$n_{2}$,则有$n_{0}=n_{2}+1$
  • 具有n个节点的完全二叉树的深度为${\left \lfloor \log_{2}{n} \right \rfloor +1}(\left \lfloor x \right \rfloor表示不大于x的最大整数)$
  • 具有n个节点的满二叉树的深度为$log_{2}{(n+1)}$

5.2 二叉树遍历方法

区分前序、中序、后序关键是访问根节点。
前序:根->左->右
中序:左->根->右
后序:左->右->根

5.2.1 前序遍历

顺序:A->B->D->G->H->C->E->I->F

5.2.2 中序遍历

顺序:G->D->H->B->A->E->I->C->F

5.2.3 后序遍历

顺序:G->H->D->B->I->E->F->C->A

5.2.4 层序遍历

顺序:A->B->C->D->E->F->G->H->I

5.2.5 遍历算法实现

typedef char ElemType;
typedef struct BiNode {
    ElemType data;
    BiNode* lChild, * rChild;
}BiNode,*BiTree;
//前序遍历
void preOrderTraverse(BiTree T)
{
    if (T == NULL)
        return;
    printf("%c", T->data);// 先打印节点信息
    preOrderTraverse(T->lChild);//再遍历左子树
    preOrderTraverse(T->rChild);//最后遍历右子树
}
//中序遍历
void inOrderTraverse(BiTree T)
{
    if (T == NULL)
        return;
    inOrderTraverse(T->lChild);
    printf("%c", T->data);
    inOrderTraverse(T->rChild);
}
//后序遍历
void postOrderTraverse(BiTree T)
{
    if (T == NULL)
        return;
    postOrderTraverse(T->lChild);
    postOrderTraverse(T->rChild);
    printf("%c", T->data);
}

5.3 红黑树

资料红黑树-维基百科

5.3.1 性质

  1. 节点非黑即红
  2. 根节点必为黑
  3. 每个叶节点(NIL节点)都是黑色
  4. 新插入节点必为红
  5. 不能有2个连续的红色节点(即父节点和子节点都为红),黑色节点不限制,或者说每一个红色节点必含黑色左右子节点
  6. 从任意一节点到叶子节点,所经过路径上黑色节点数目相同
title: NIL节点
1. NIL节点也就是NULL节点
2. 在红黑树中用NIL节点填补原节点中缺失的左右子节点
3. 在红黑树中叶子节点指的是NIL节点

5.3.2 操作

红黑树插入删除操作会导致不满足红黑树性质,这时可以通过旋转(左旋、右旋),重新着色操作来维持红黑树。 下图展现了红黑树插入的几种情况