算法工程师之基本算法学习一

算法工程师学习

  • 第一部分 排序算法
    • 一、快排
    • 二、归并
    • 三、计数
  • 第二部分
    • 一、回溯
    • 二、BFS算法
      • 三、滑动窗口
      • 四、动态规划
    • 二、递归
      • 1. 反转链表
    • 三、剪枝

第一部分 排序算法

一、快排

  1. 删除排序数组中的重复项:利用双指针(int i = 0, j = 1;),将不重复的元素向左移动
  2. 股票交易的最佳时机Ⅱ:每天购买,第二天比当天收益高,真实买入,否则不买
  3. 回文串:c++内置函数
  4. 有限状态机

当问题要讨论的情况较小时,可以使用if或case条件分支

情况较多时,用状态机模型,每个时刻都有一个状态s1,然后根据输入值in_Val转移到下一状态s2,找出所有状态转换关系
① 建立状态表,可以通过hash表,如unordered_map> table;或用一个二维数组也行
② 根据当前不同的状态,确定下一状态的函数get_s()
③ 对不同的状态进行处理 函数 get()

  1. 二叉树的最大深度:为max(root->left, root->right) + 1;
  2. 广度优先搜索(Breadth-First Search,BFS):层层遍历,利用队列储存每层的数据,然后根据队列先进后出(FIFO)的特性,依次弹出
  3. 深度优先搜索

二、归并

// priority_queue 默认 为 大顶堆

priority_queue <int,vector<int>,greater<int> > q; // 升序队列,小顶堆

priority_queue <int,vector<int>,less<int> >q; // 降序队列,大顶堆

//greater和less是std实现的两个仿函数
// 需要注意的是,如果使用less<int>和greater<int>,需要头文件:#include 

三、计数

第二部分

来源微信公众号labuladong

一、回溯

回溯定义:相当于遍历整个决策树,穷举出所有可能的结果,深度优先算法(DFS,Depth First Search)每个分支深入到底

def backtrack(路径, 选择列表):
	if 满足结束条件:
	result.insert(路径)
	return;
	
// 回溯的核心模块
for 选择 in 选择列表
	#做选择
	将选择从列表中移除
	路径.insert(选择);
	backtrack(路径, 选择列表)

	#撤销选择
	路径.remove(选择);
	将该选择再加入选择列表

常见题型:全排列,N皇后,以及组合和子集问题,注意要添加判断避免访问同一节点

// 寻找不重复数组中 所有的子集
vector<vector<int>> res;
vector<vector<int>> subset(vector<int>& nums) {
     
	vector<int> path;
	backtrack(nums, 0, path);
	return res;
}
void backtrack(vector<int>& nums, int start, vector<int>& path) {
     
	res.push_back(path); // 每次将path中的结果添加到res中
	for(int i = start; i < nums.size(); i++) {
      // start开始,排除已选择过的数字
		path.push_back(nums[i]); // 做选择
		backtrack(nums, i+1, path); // 开始回溯
		path.pop_back(); // 取消选择
	}
}

二、BFS算法

广度优先算法(Breadth-First Search,BFS)要找从Start到target的最短路径,空间复杂度比DFS高;
一般利用队列,每次将一个节点周围的所有节点加入队列。

// BFS框架
int BFS(TreeNode* start, TreeNode* target) {
     
	queue<TreeNode* > q;
	set<TreeNode* > visited;  // 避免走回头路
	q.push(start); // 将起点加入队列
	visited.insert(start);
	int step; // 记录扩散步数
	while(!q.empty()) {
     
		for(int i = 0; i < q.size(); i++) {
     
			TreeNode* tmp = q.front(); q.pop();
			if(tmp == target)	return step; // 判断是否到达终点
			for(TreeNode* x : tmp.adj()) {
      // 将当前节点tmp的 相邻节点加入队列 
				if(visited.count(x) == 0) {
     q.push(x); visited.insert(x);}
			}
		}
		step++; // 更新步数
	}
	return step;
}

三、滑动窗口

算法大致逻辑如下

int left = 0, right = 0;

while (right < s.size()) {
     
    // 增大窗口
    window.add(s[right]);
    right++;

    while (window needs shrink) {
     
        // 缩小窗口
        window.remove(s[left]);
        left++;
    }
}

滑动窗口的时间复杂度为O(n),比较高效
最小覆盖子串解法:needs和window相当于计数器,分别记录T中字符出现次数和「窗口」中的相应字符的出现次数。
思路:如果一个字符进入窗口,应该增加window计数器;如果一个字符将移出窗口的时候,应该减少window计数器;当valid满足need时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

string minWindow(string s, string t) {
     
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;

    int left = 0, right = 0;
    int valid = 0;
    // 记录最小覆盖子串的起始索引及长度
    int start = 0, len = INT_MAX;
    while (right < s.size()) {
     
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        if (need.count(c)) {
     
            window[c]++;
            if (window[c] == need[c])
                valid++;
        }

        // 判断左侧窗口是否要收缩
        while (valid == need.size()) {
     
            // 在这里更新最小覆盖子串
            if (right - left < len) {
     
                start = left;
                len = right - left;
            }
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            if (need.count(d)) {
     
                if (window[d] == need[d])
                    valid--;
                window[d]--;
            }                    
        }
    }
    // 返回最小覆盖子串
    return len == INT_MAX ? "" : s.substr(start, len);
}

四、动态规划

动态规划般用于求最值,如“最小增长子序列”;关键是穷举,但是存在着重叠子序列,利用【DP table】避免重复的部分
框架:「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。
dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]

// 0-1背包问题:物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0

for i in [1..N]:
    for w in [1..W]:
        dp[i][w] = max(
            把物品 i 装进背包,
            不把物品 i 装进背包
        )
return dp[N][W]

// C++代码
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
     
    // vector 全填入 0,base case 已初始化
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) {
     
        for (int w = 1; w <= W; w++) {
     
            if (w - wt[i-1] < 0) {
     
                // 当前背包容量装不下,只能选择不装入背包
                dp[i][w] = dp[i - 1][w];
            } else {
     
                // 装入或者不装入背包,择优
                dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], 
                               dp[i - 1][w]);
            }
        }
    }

    return dp[N][W];
}

二、递归

递归需要明确递归函数的定义/功能

1. 反转链表

// 递归方法的基本代码
ListNode* reverse(ListNode* head) {
      // 反转整个链表
	if(head->next == NULL)	return head; // 只有一个节点时,反转之后还是自己
	ListNode* last = reverse(head->next);
	head->next->next = head; 
	head->nxet = NULL;
	return last; // 返回反转之后的头结点last,head已经变成尾结点了
}

// 反转链表的一部分 区间 [m, n]之间的链表
ListNode* sucessor = NULL;
ListNode* reverseBetween(ListNode* head, int m, int n) {
     
	if( m == 1) {
     
		return reverseN(head, n); // 到达第m个节点后,相当于反转前n-m个节点
	}
	head->next = reverseBetween(head->next, m-1, n-1); // 将头结点移动到第m个节点
	return head;
}
ListNode* reverseN(ListNode* head, int n) {
      // 反转链表的前N个节点
	if( n == 1) {
     
		sucessor = head->next; // 后继节点记录第n+1个节点
		return head;
	}
	ListNode* last = reverseN(head, n-1);
	head->next->next = head;
	head->next = sucessor; // 反转之后的head节点 与 第n+1个节点相连
	return last;
}

判断回文链表的思路:从两端向中间收缩
方法:① 构造一条新的反转链表; ②利用链表的后序遍历,用栈结构;③ 利用快慢指针,找到链表的中点,反转后半部分,可以将空间复杂度降为O(1)

三、剪枝

动态规划:一般用于求最值,如“最小增长子序列”;关键是穷举,但是存在着重叠子序列,利用【DP table】避免重复的部分
三要素:重叠子问题、最优子结构、状态转移方程
最优子结构:只有当子问题相互独立时,才有最优子结构
状态转移方程:当前状态dp[i]与其他状态的关系

你可能感兴趣的:(C++语言,新手学习,算法,链表)