作者归档:limingfg

leetcode 算法14天的自我总结

结论在前

1、我做算法题的目的是加深一下数据结构与算法的知识


2、不是做题就有效果,里面数据结构的基础好重要,没有这些数据的认识,也就是缺少了这部分的认知,你想10年都不一定有效果


3、题目需要看懂,不要拿起题目就分析用什么动态规划,递归虽然是必要的,但读懂题目更清楚用什么数据结构与算法去解决而不是做到差不多又重来


4、一定一定要拿出纸笔来描述算法的过程,画图这个真的好重要,分析能力的体验,我当时做的时候以为真的好简单,之后做了下,发现有边界情况,然后打补丁,不断提交,不断补丁导致这个程序变成,以针对特殊情况去解决问题,这个真的好像我们写业务代码一样,没有模式可言


5、做不出来,就不要死扣了等,直接看答案吧,这种不是怀疑自己智力,当你看完答案会有醍醐灌顶的效果,不懂看多几次


6、形成套路,这种抽象总结不是要你背诵题目,而是这些本来是需要了解,总结,抽象,公式化的东西,大脑是懒的我们需要更聪明地节省时间

题思考

1、Two Sum

https://leetcode-cn.com/problems/two-sum/
给你一个数组,里面是整数,还有给你一个目标数,寻址两数相加等于目标数
我的思路:我突然就想找出有多少个组合,并没有看懂题意,事实里面可能有很多组合如,[-2,3,4,2,1] , 目标数位 5,这个时候,组合有 [3,2], [4,1] 我的思考就去了这里,题目说只是说找出,没有说找全部 因此总结,题没有总结


解法:

1、两个循环,nxn 地循环 找出等于目标数
2、哈希表,这个思想是用空间换时间,在循环里面找到由目(标数 – 循环当前的数) 得到 0 的key在哈希表里面存在就说明找到这两个数

func twoSum(nums []int, target int) []int {
hashTable := map[int]int{}
for i, x := range nums {
if p, ok := hashTable[target-x]; ok {
return []int{p, i}
}
hashTable[x] = i
}
return nil
}


2、二分查找法
https://leetcode-cn.com/problems/binary-search/


我的思考:这本来熟悉到不熟悉,但实在没有多用忘记的忘记,其实这个在真的在业务开发用的很少?其实真是,你说排行榜,基本上你用上redis,或者几个循环就搞定了,就不需要太多,但当没有这些可以满足你的时候,你可能会寻找解决方案,你会查看他们源码,基本是用上了这些标准数据结构库进行处理,无论怎样变基本的东西都在这里
这个题意没有,但是鉴于忘记了这个算法,需要补充基本知识

func search(nums []int, target int) int {
    left, right := 0, len(nums)-1
    for left <= right {
        pivot := left + (right - left)
        if nums[pivot] == target {
            return pivot
        }
        if target < nums[pivot] {
            right = pivot - 1
        } else {
            left = pivot + 1
        }
    }
    return -1
}

3、278. 第一个错误的版本 https://leetcode-cn.com/problems/first-bad-version/

我的分析:题意没有理解,实例没有看明白,其实本地说的是调用内置函数,找出那几个版本是有问题的,这个就是考二分查找法

func firstBadVersion(n int) int {
    left, right := 1, n;
    for left < right { // 循环直至区间左右端点相同
         mid := left + (right - left) / 2; // 防止计算时溢出
        if (isBadVersion(mid)) {
            right = mid; // 答案在区间 [left, mid] 中
        } else {
            left = mid + 1; // 答案在区间 [mid+1, right] 中
        }
    }
    // 此时有 left == right,区间缩为一个点,即为答案
    return left;
}

4、35. 搜索插入位置 https://leetcode-cn.com/problems/search-insert-position/

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。 输入: nums = [1,3,5,6], target = 5 输出: 2
我的分析:好像好简单,一个循环搞定,但是人家是给了一个循序数组给你,这个也是二分查找法最快,在工作上凡事需要查找某个值,建议还是先排好序,出了利用好算法外,还利用好数组里面的局部性,也就是加载到高速缓存上,更快给到CPU运算

func searchInsert(nums []int, target int) int {
    left, right := 0, len(nums)-1
    mid := left


    for left <= right {
        mid = left + (right-left)/2
        fmt.Println(nums[mid], left, mid, right)
        if nums[mid] == target {
            return mid
        } else if nums[mid] > target {
            right = mid - 1
        } else if nums[mid] < target {
            left = mid + 1
        }
    }


    if nums[mid] < target {
        return mid + 1
    } else {
        return mid
    }
}

4、977. 有序数组的平方 https://leetcode-cn.com/problems/squares-of-a-sorted-array/


给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]


有多种解法

https://leetcode-cn.com/problems/squares-of-a-sorted-array/solution/you-xu-shu-zu-de-ping-fang-by-leetcode-solution/https://leetcode-cn.com/problems/squares-of-a-sorted-array/solution/dai-ma-sui-xiang-lu-shu-zu-ti-mu-zong-ji-1rtz/

方法一:直接排序

方法二:双指针


我的分析:我是直接循环求平方,在排序,但是效果不好, 时间复杂度:O(n) 空间复杂度:O(logn)
看了答案后,发现人家是通过借助变量在同一个循环里面,找最大最小值,题目里面已经说明 非递减顺序 排序的整数数组,通过左右指针移动,指向的平方后比对大小看看,那个大在放到某个数组里面

func sortedSquares(nums []int) []int {
    n := len(nums)
    ans := make([]int, n)
    i, j := 0, n-1
    for pos := n - 1; pos >= 0; pos-- {
        if v, w := nums[i]*nums[i], nums[j]*nums[j]; v > w {
            ans[pos] = v
            i++
        } else {
            ans[pos] = w
            j--
        }
    }
    return ans
}


复杂度分析


时间复杂度:O(n),其中 nn 是数组 nums 的长度。


空间复杂度:O(1)。除了存储答案的数组以外,我们只需要维护常量空间。

5、189. 旋转数组 https://leetcode-cn.com/problems/rotate-array/


给定一个数组,将数组中的元素向右移动k个位置,其中k是非负数。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 你可以使用空间复杂度为O(1) 的原地算法解决这个问题吗?
输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右旋转 1 步: [7,1,2,3,4,5,6] 向右旋转 2 步: [6,7,1,2,3,4,5] 向右旋转 3 步: [5,6,7,1,2,3,4]

我的分析:第一次看不明白题目,看多次,原来是循环移动,所以这里就会出现mod的计算,如上面的例子中,7移动由6到0,1由0到1,除了mod之后还需要临时变量保存做交换

https://leetcode-cn.com/problems/rotate-array/solution/dai-ma-sui-xiang-lu-189-xuan-zhuan-shu-z-yb1c/

func rotate(nums []int, k int) []int {
    n := len(nums)
    k %= n


    for start, count := 0, gcd(k, n); start < count; start++ {
        pre, cur := nums[start], start
        // fmt.Println("1=1", pre, cur, start)
        for ok := true; ok; ok = cur != start {
            next := (cur + k) % n
            nums[next], pre, cur = pre, nums[next], next
            // fmt.Println("2=2", nums, next, pre, cur)
        }
    }
    return nums
}


func gcd(a, b int) int {
    for a != 0 {
        a, b = b%a, a
    }
    return b
}

6、283. 移动零
https://leetcode-cn.com/problems/move-zeroes/

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12] 输出: [1,3,12,0,0] 说明:
必须在原数组上操作,不能拷贝额外的数组。 尽量减少操作次数。


我的分析:无思路,直接看答案,得知用双指针能解决,思路为

图解https://leetcode-cn.com/problems/move-zeroes/solution/dong-hua-yan-shi-283yi-dong-ling-by-wang_ni_ma/

7、344. 反转字符串 https://leetcode-cn.com/problems/reverse-string/


编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。


你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
输入:[“h”,”e”,”l”,”l”,”o”] 输出:[“o”,”l”,”l”,”e”,”h”]


我的分析:双指针交换

func reverseString(s []byte)  {
    j := len(s) - 1
    i := 0
    for i < j  {
        s[i], s[j] = s[j], s[i]
        i += 1
        j -= 1
    }
}

8、557. 反转字符串中的单词 III

https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
示例:
输入:”Let’s take LeetCode contest” 输出:”s’teL ekat edoCteeL tsetnoc”
我的分析,循环每个字符直到遇到空格为止,然后根据这个位置倒序地把字符赋值变量,然后这个变量添加这个空格

func reverseWords(s string) string {
    length := len(s)
    ret := []byte{}
    for i := 0; i < length; {
        start := i
        for i < length && s[i] != ' ' {
            i++
        }
        for p := start; p < i; p++ {
            ret = append(ret, s[start+i-1-p])
        }
        for i < length && s[i] == ' ' {
            i++
            ret = append(ret, ' ')
        }
    }
    return string(ret)
}

9、876. 链表的中间结点

https://leetcode-cn.com/problems/middle-of-the-linked-list/


给定一个头结点为 head的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.


我的分析,这个看似是知道数量取中值,但是这个是链表,需要把所有元素都循环一次,效率为O(N),空间为O(N)(单指针法可以节省)
我查看了答案后,发现原来有快慢指针,这个真的让我刮目双看,slow 一次走一步,fast 一次走两步,当 fast 到达链表的末尾时,slow 必然位于中间

https://leetcode-cn.com/problems/middle-of-the-linked-list/solution/lian-biao-de-zhong-jian-jie-dian-by-leetcode-solut/

func middleNode(head *ListNode) *ListNode {
    slow := head;
    fast := head;
    for fast != nil && fast.Next != nil {
        slow = slow.Next;
        fast = fast.Next.Next;
    }
    return slow;
}

10、19. 删除链表的倒数第 N 个结点

https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 进阶:你能尝试使用一趟扫描实现吗?


输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]
我的分析,链表一个特性是找一个元素需要遍历,关于顺序我们可以通过快慢指针实现,通过一个哑变量来定位那个元素

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    lenN := getNodesLen(head)
    // fmt.Println(lenN)
    dummy := &ListNode{0, head}
    cur := dummy
    for i:=0; i < lenN - n; i+=1 {
        cur = cur.Next
    } 
    // if cur.Next != nil {
        cur.Next = cur.Next.Next
    // } else {
    //     head = nil
    // }
    return dummy.Next
}


func getNodesLen(head *ListNode) int {
    n:=0
    for ; head !=nil; head = head.Next {
        n+=1
    }
    return n
}

11、3. 无重复字符的最长子串


https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/


我的分析,我没有做出来,看完答案后,了解滑动窗口,由第一个字符开始,不断扩大自己的位数直到重复字符出现

func lengthOfLongestSubstring(s string) int {
    // 哈希集合,记录每个字符是否出现过
    m := map[byte]int{}
    n := len(s)
    // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
    rk, ans := -1, 0
    for i := 0; i < n; i++ {
        if i != 0 {
            // 左指针向右移动一格,移除一个字符
            delete(m, s[i-1])
        }
        for rk + 1 < n && m[s[rk+1]] == 0 {
            // 不断地移动右指针
            m[s[rk+1]]++
            rk++
        }
        // 第 i 到 rk 个字符是一个极长的无重复字符子串
        ans = max(ans, rk - i + 1)
    }
    return ans
}


func max(x, y int) int {
    if x < y {
        return y
    }
    return x
}

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/wu-zhong-fu-zi-fu-de-zui-chang-zi-chuan-by-leetc-2/

12、733. 图像渲染

https://leetcode-cn.com/problems/flood-fill/


有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标(sr, sc)表示图像渲染开始的像素值(行 ,列)和一个新的颜色值newColor,让你重新上色这幅图像。
为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。
最后返回经过上色渲染后的图像。
示例 1:
输入: image = [[1,1,1],[1,1,0],[1,0,1]] sr = 1, sc = 1, newColor = 2 输出: [[2,2,2],[2,2,0],[2,0,1]] 解析: 在图像的正中间,(坐标(sr,sc)=(1,1)), 在路径上所有符合条件的像素点的颜色都被更改成2。 注意,右下角的像素没有更改为2, 因为它不是在上下左右四个方向上与初始点相连的像素点。

func floodFill(image [][]int, sr int, sc int, newColor int) [][]int {
    lenc := len(image[0])
    lenr := len(image)


    if sr > lenr-1 || sc > lenc-1 {
        return image
    }


    centerColor := image[sr][sc]


    if centerColor == newColor {
        return image
    }


    // 上左下右顺序深度遍历
    image[sr][sc] = newColor


    // 记录遍历的点
    queue := [][]int{{sr, sc}}
    // fmt.Println(queue, image)
    // 从rc rs 出发
    for i := 0; i < len(queue); i++ {
        pos := queue[i]


        // 上
        if pos[0]-1 >= 0 && image[pos[0]-1][pos[1]] == centerColor {
            // fmt.Println("上")
            queue = append(queue, []int{pos[0] - 1, pos[1]})
            image[pos[0]-1][pos[1]] = newColor
        }
        // 右
        if pos[1]+1 < lenc && image[pos[0]][pos[1]+1] == centerColor {
            // fmt.Println("右")
            queue = append(queue, []int{pos[0], pos[1] + 1})
            image[pos[0]][pos[1]+1] = newColor
        }
        // 下
        if pos[0]+1 < lenr && image[pos[0]+1][pos[1]] == centerColor {
            // fmt.Println("下")
            queue = append(queue, []int{pos[0] + 1, pos[1]})
            image[pos[0]+1][pos[1]] = newColor
        }
        // 左
        if pos[1]-1 >= 0 && image[pos[0]][pos[1]-1] == centerColor {
            // fmt.Println("左")
            queue = append(queue, []int{pos[0], pos[1] - 1})
            image[pos[0]][pos[1]-1] = newColor
        }
        // fmt.Println(queue)
    }
    return image
}

13、617. 合并二叉树

https://leetcode-cn.com/problems/merge-two-binary-trees/

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

示例 1:

输入: 
    Tree 1                     Tree 2                  
          1                         2                             
         / \                       / \                            
        3   2                     1   3                        
       /                           \   \                      
      5                             4   7                  
输出: 
合并后的树:
         3
        / \
       4   5
      / \   \ 
     5   4   7
注意: 合并必须从两个树的根节点开始

我的分析,使用递归

func mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode {
    if root1 == nil {
        return root2
    }


    if root2 == nil {
        return root1
    }


    root1.Val += root2.Val
    root1.Left = mergeTrees(root1.Left, root2.Left)
    root1.Right = mergeTrees(root1.Right, root2.Right)


    return root1
}

14、116. 填充每个节点的下一个右侧节点指针

https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {   int val;   Node *left;   Node *right;   Node *next; } 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。

进阶:
你只能使用常量级额外空间。 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。


我的分析,广度遍历,进入下一个节点,把左右节点相连,然后在循环下一层在相连

func connect(root *Node) *Node {
    if root == nil {
        return root
    }
    leftNode := root


    for leftNode.Left != nil {
        node := leftNode


        // 遍历层
        for node != nil {
            // 左右节点连接
            node.Left.Next = node.Right


            if node.Next != nil {
                node.Right.Next = node.Next.Left
            }
            
            node = node.Next
        }


        leftNode = leftNode.Left
    }


    return root
}

15、21. 合并两个有序链表
https://leetcode-cn.com/problems/merge-two-sorted-lists/


将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]


分析:递归方式,比对大小,停止条件

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
    if l1 == nil {
        return l2
    }


    if l2 == nil {
        return l1
    }


    if l1.Val < l2.Val {
        l1.Next = mergeTwoLists(l1.Next, l2)
        return l1
    } else {
        l2.Next = mergeTwoLists(l1, l2.Next)
        return l2
    }
}

16、206. 反转链表

https://leetcode-cn.com/problems/reverse-linked-list/

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

输入:head = [1,2,3,4,5]输出:[5,4,3,2,1]

分析:递归,停止条件为这个节点的是否空或下一点为空

func reverseList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }

    cur := reverseList(head.Next)
    head.Next.Next = head
    head.Next = nil
    return cur
}

17、77. 组合


https://leetcode-cn.com/problems/combinations/

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
 示例 1:
输入:n = 4, k = 2输出:[  [2,4],  [3,4],  [2,3],  [1,2],  [1,3],  [1,4],]


分析,这题我做不到,这个需要用回溯算法

https://leetcode-cn.com/problems/combinations/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-hui-s-0uql/ 

这个题解能够更好了解回溯算法,期重点是,找递归条件,还有一个出栈做回溯

func combine(n int, k int) [][]int {
    res := [][]int{}


    res = backtrack(n, k, 1, []int{}, res)


    return res
}


func backtrack(n, k, start int, track []int, res [][]int) [][]int {
    if len(track) == k {
        tmp := make([]int, k)
        copy(tmp, track)
        res = append(res, tmp)
    }


    if len(track)+n-start+1 < k {
        return res
    }


    for i := start; i <= n; i++ {
        track = append(track, i)
        res = backtrack(n, k, i+1, track, res)
        track = track[:len(track)-1]
    }
    return res
}

18、784. 字母大小写全排列

https://leetcode-cn.com/problems/letter-case-permutation/

给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
示例: 输入:S = “a1b2” 输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
输入:S = “3z4” 输出:[“3z4”, “3Z4”]
输入:S = “12345” 输出:[“12345”]


分析:这个我完全做出来,但是不够简洁,看答案,二分掩码,递归


https://leetcode-cn.com/problems/letter-case-permutation/solution/zi-mu-da-xiao-xie-quan-pai-lie-by-leetcode/

func letterCasePermutation(s string) []string {
    slen := len(s)
    letters := [][]byte{}
    i := 0
    for i < slen {
        if check_is_letter(s[i]) {
            lower := lower_letter(s[i])
            upper := upper_letter(s[i])
            // fmt.Println(string(lower), string(upper))
            if len(letters) == 0 {
                letters = [][]byte{{lower}, {upper}}
            } else {
                tmp := make([][]byte, len(letters))
                copy(tmp, letters)


                letters = [][]byte{}
                for _, letter := range tmp {
                    letter = append(letter, lower)
                    tmpl := make([]byte, len(letter))
                    copy(tmpl, letter)
                    letters = append(letters, tmpl)
                    // fmt.Println("lower", string(tmpl))
                }


                for _, letter := range tmp {
                    letter = append(letter, upper)
                    tmpl := make([]byte, len(letter))
                    copy(tmpl, letter)
                    letters = append(letters, tmpl)


                    // fmt.Println("upper", string(letter))
                }
                // fmt.Println("letter:", letters)
            }
        } else {
            if len(letters) == 0 {
                letters = [][]byte{{s[i]}}
            } else {
                for j, letter := range letters {
                    letter = append(letter, s[i])
                    letters[j] = letter
                    // fmt.Println("not letter", string(letter))
                }
            }
        }
        i++
    }


    snew := []string{}
    for i := 0; i < len(letters); i++ {
        snew = append(snew, string(letters[i]))
    }


    return snew
}


func check_is_letter(s byte) bool {
    if s >= 65 && s <= 90 || s >= 97 && s <= 122 {
        return true
    } else {
        return false
    }


}


func lower_letter(s byte) byte {
    if s >= 65 && s <= 90 {
        return byte(s + 32)
    }


    return s
}


func upper_letter(s byte) byte {
    if s >= 97 && s <= 122 {
        return byte(s - 32)
    }
    return s
}

19、70. 爬楼梯

https://leetcode-cn.com/problems/climbing-stairs/

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。 1.  1 阶 + 1 阶 2.  2 阶 示例 2:
输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1.  1 阶 + 1 阶 + 1 阶 2.  1 阶 + 2 阶 3.  2 阶 + 1 阶


分析:动态规划,矩阵快速幂。。这个对我来说比较新鲜,化了很多时间了解

https://leetcode-cn.com/problems/climbing-stairs/solution/pa-lou-ti-by-leetcode-solution/

func climbStairs(n int) int {
    p, q, r := 0, 0, 1
    for i := 1; i <= n; i++ {
        p = q
        q = r
        r = p + q
    }
    return r
}



20、198. 打家劫舍

https://leetcode-cn.com/problems/house-robber/


你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。      偷窃到的最高金额 = 1 + 3 = 4 。 示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。      偷窃到的最高金额 = 2 + 9 + 1 = 12 。

分析:这条题目,好特别低介绍了如何对比两个前后指针变量总和来判断那个金额最高

func rob(nums []int) int {
    if len(nums) == 0 {
        return 0
    }


    if len(nums) == 1 {
       return nums[0]
    }


    p := nums[0]
    q := nums[1]
    if p > q {
        q = p
    }
    for i :=2; i < len(nums); i++ {
        tmp := q
        if p + nums[i] > q {
            q = p + nums[i]
        }
        p = tmp
    }


    return q
}

后面的是位移的题目


231. 2 的幂
https://leetcode-cn.com/problems/power-of-two/


191. 位1的个数

https://leetcode-cn.com/problems/number-of-1-bits/


190. 颠倒二进制位

https://leetcode-cn.com/problems/reverse-bits/


136. 只出现一次的数字

https://leetcode-cn.com/problems/single-number/


都是强调位移的特性,并,与,异或,运算的逻辑,190. 颠倒二进制位 比较有难度

基本上做不出的

46. 全排列 https://leetcode-cn.com/problems/permutations/


567. 字符串的排列 https://leetcode-cn.com/problems/permutation-in-string/


后续迭代


后续需要加强点有4个


1、重温数据结构与常规算法,把基础打号


2、每天找相关的题目来看看是否掌握


3、看动态规划,还有回溯算法,这类虽然对于日常工作用的不多主要是场景问题,如果要你做一个排班的问题,这个回溯算法就很有别要了

4、多搜索看看这些算法数据结构的实际用途

资料

1、极客时间,王峥算法,下面是我推荐连接

https://time.geekbang.org/column/intro/126?code=CLPyMszxrMlJaEgfJlmZNV9Qj77XsVva-DZ2BAcvJWc%3D&utm_term=SPoster&page=A

2、代码随想录

https://space.bilibili.com/525438321?from=search&seid=10425904522036290503

3、有条件的看youtu

负载均衡简要总结

无负载均衡

定义

旨意是位分布一系列任务到到一系列资源进行处理,目的是为了提高处理效率 关于任务,需要考虑任务的执行时间 任务的大小,任务依赖(任务调度metaheuristic),任务分发(Tree-Shaped Computation),这个决定了执行时间

算法

静态算法

不需要考虑处理任务的处理状态,分发给计算单元就行,也是有一定统计每个计算单元的任务量,优点是使用简单 代表算法 1、轮询请求(Round robin), 想象成一个循环链表,指针代表当前访问的节点,接收任务一次,指针移动一次指向下一个
2、随机请求(random),也就是随机函数挑选一个,也可以加上权重 3、哈希请求(hash), 通过hash函数,指派任务到某一组计算单元 4、权重(weight),设置计算单元,那些可以接收多些任务进行选取请求,这个也可以结合 轮询,会使分配更加均衡 5、最小工作(连接)请求(less,work) , 最小请求的计算单元,指派更多任务

动态算法

不同的计算单元需要交换状态信息,但效率会比较低需要收集多个计算单元负载情况 代表算法 Master-worker 通过一个主指派任务给每个worker,面对大量任务master 会称为瓶颈
非分层架构,个master-worker 相反,它利用一种叫做work stealing 的方式,指某个线程从其他队列里窃取任务来执行,这种思想是因为有些线程给某些线程更快处理完任务,可以把另外线程的任务队列拿来进行处理 在某些情况下还是会存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源, 比如创建多个线程和多个双端队列。 Work stealing 最大效益的是,每个任务计算不需要依赖其它任务完成,不然就变等待,这样效率也不会变高


平均负载算法

收集每个节点的负载情况,首先计算出集群各个节点的负载富余的平均值,然后求方差,如果结果越小则说明集群负载越均衡,状态越健康,越大则说明越不均衡,集群的负载均衡处于一个相对不健康的状态 基于网络响应时间(mongodb客户端),基于网络我们需要考虑 可信度问题,也就是需要考虑网络波动问题 例如:m0离当前时间最近,认为m0的可信度最高,所以给了36%的权重


基于负载值

例如:m表示计算得到的负载值,m0表示当前获取到的节点负载,m1表示上一秒获取到的节点负载,参数D是可信度系数,表示上一秒的节点负载值对当前负载值的影响程度,越大表示影响越小
并行硬件负载均衡设计 硬件并行计算任务需要考虑 1、异构计算机组成 2、共享与分布式内存,通过分布式内存与消息通知协调处理单元 3、分层处理”Master-Worker”,通过这个模式,master 指派任务到不同的计算单元硬件上 4、易扩展 5、容错

基于网络层面考虑

四层(TCP 和 UDP)是基于连接做流量调度,TCP Keepalive 保持长连接 七层(HTTP/HTTPS)是基于请求做调度。比如 http get 请求访问一个页面。 四层 vs 七层 四层,客户端和Real Server仅仅只建立了一条连接,负载均衡器仅仅只起着转发这样的一个功能,只不过在转发的过程当中将IP报文头做个修改。但是连接始终还是一条连接 七层,负载均衡器对负载均衡要求更加高,因为一个正常连接都会建立两条独立的TCP连接。由于建立了两条TCP连接 七层返回数据模式:Real Server将请求处理完毕之后纯粹的将请求发回给负载均衡处理,负载均衡处理器再将请求转发给客户端,正常的TCP连接基于七层的,进要经过负载均衡器出也要经过负载均衡器,就是进出都要经过负载均衡器 四层返回数据模式:请求客户端到达服务端之后,服务端响应返回数据有两种方式。一种是直接将数据返回给客户端,而不经过中间的四层负载均衡器,这是经常使用的DR方式

负载均衡系统设计特点

请求的分发        

静态算法         动态算法

会话保持    

源IP地址的持续性保持      cookie 持续性保持      基于HTTP报文头的持续性保持

服务健康监测    

利用ICMP、TCP、HTTP、FTP等协议进行检测,即主要是在传输层和应用层进行检测     

故障隔离    出现问题剔除

自动恢复    自动注册

均衡的应用

DNS负载均衡(DNS RR记录)全局负载均衡(GSL.B,Global Server Load Balance)主要的目的是在整个网络范围内将用户的请求定向到最近的节点(或者区域) 基于DNS实现、基于重定向实现、基于路由协议实现 特点:能通过判断服务器的负载,包括CPU占用、带宽占用等数据,决定服务器的可用性,同时能判断用户(访问者)与服务器间的链路状况,选择链路状况最好的服务器
优点使用简单:负载均衡工作,交给DNS服务器处理,省掉了负载均衡服务器维护的麻烦提高性能:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能; 缺点可用性差:DNS解析是多级解析,新增/修改DNS后,解析时间较长;解析过程中,用户访问网站将失败;扩展性低:DNS负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展;维护性差:也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载)

IP负载均衡(反向代理)在网络层通过修改请求目标地址进行负载均衡 真实服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器,再将数据包源地址修改为自身的ip地址,发送给用户浏览器 IP负载均衡,真实物理服务器返回给负载均衡服务器,存在两种方式:
(1)负载均衡服务器在修改目的ip地址的同时修改源地址。将数据包源地址设为自身盘,即源地址转换(snat)。
(2)将负载均衡服务器同时作为真实物理服务器集群的网关服务器。
优点:
(1)在内核进程完成数据分发,比在应用层分发性能更好;
缺点:
(2)所有请求响应都需要经过负载均衡服务器,集群最大吞吐量受限于负载均衡服务器网卡带宽;

链路层负载均衡

数据分发时,不修改ip地址,指修改目标mac地址,配置真实物理服务器集群所有机器虚拟ip和负载均衡服务器ip地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的。 实际处理服务器ip和数据请求目的ip一致,不需要经过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR模式) DR模式 意思是 后端服务直接跟服务端进行数据交互 实现方式,多个后端服务器 + 一个入口(LVS)形成一个VIP入口,然后入口接收请求,转发给后端服务器(端口需要一致 客户访问80,后端服务也是80)然后入口把mac地址改为客户端把包交给后端服务器,通过修改数据包进行数据均衡,这个时候入口会记录这个配对

负载均衡请求不均衡的原因

1、长链某些节点 2、后端某些节点响应异常 如 开启了会话保持功能 健康检查异常 TCP Keepalive 保持长连接
加权最小连接数(WLC)调度方式和会话保持 -> 尝试改为加权加权轮询(WRR)算法和会话保持

https://developer.qiniu.com/qvm/kb/5144/the-common-problems-in-load-balancing


工具

分类

七层代理 nginx haproxy

四层代理 lvs,haproxy, F5

CDN,分布式内容


解析

LVS

是基于Linux操作系统实现的一种软负载均衡,四层的IP负载均衡技术 LVS+Keepalived 对 MySQL 主从做负载均衡
LVS的优点 抗负载能力强、是工作在网络4层之上仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的,对内存和cpu资源消耗比较低 缺点 软件本身不支持正则表达式处理,不能做动静分离;而现在许多网站在这方面都有较强的需求,这个是Nginx/HAProxy+Keepalived的优势所在

HAProxy

补充Nginx的一些缺点,比如支持Session的保持,Cookie的引导;同时支持通过获取指定的url来检测后端服务器的状态。 支持TCP协议的负载均衡转发,可以对MySQL读进行负载均衡,对后端的MySQL节点进行检测和负载均衡,大家可以用LVS+Keepalived对MySQL主从做负载均衡
HAProxy 是基于第三应用实现的软负载均衡,Haproxy 是基于四层和七层技术 HAProxy 负载均衡策略非常多:Round-robin(轮循)、Weight-round-robin(带权轮循)、source(原地址保持)、RI(请求URL)、rdp-cookie(根据cookie)static-rr,表示根据权重

Nginx

Nginx 优点:基于系统与应用的负载均衡,能够更好地根据系统与应用的状况来分配负载 缺点:负载能力受服务器本身性能的影响,仅能支持http、https和Email协议
Nginx 实现负载均衡的分配策略, 轮询(默认):weight:指定轮询几率。  ip_hash:每个请求按访问 ip 的 hash 结果分配。  fair(第三方):按后端服务器的响应时间来分配请求,响应时间短的优先分配。  url_hash(第三方):按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,后端服务器为缓存时比较有效。

F5

优点:能够直接通过智能交换机实现,处理能力更强,而且与系统无关,负载性能强,更适用于一大堆设备、大访问量、简单应用。

缺点:成本高,除设备价格高昂,而且配置冗余,很难想象后面服务器做一个集群,但最关键的负载均衡设备却是单点配置,无法有效掌握服务器及应用状态。负载均衡功能

F5 BIG-IP用作HTTP负载均衡器的主要功能:

1、F5 BIG-IP提供12种灵活的算法将所有流量均衡的分配到各个服务器,而面对用户,只是一台虚拟服务器。

2、F5 BIG-IP可以确认应用程序能否对请求返回对应的数据。假如F5 BIG-IP后面的某一台服务器发生服务停止、死机等故障,F5会检查出来并将该服务器标识为宕机,从而不将用户的访问请求传送到该台发生故障的服务器上。这样,只要其它的服务器正常,用户的访问就不会受到影响。宕机一旦修复,F5 BIG-IP就会自动查证应用保证对客户的请求作出正确响应并恢复向该服务器传送。

3、F5 BIG-IP具有动态Session的会话保持功能,笔者也是在网站中使用的F5将用户IP与Session通过F5进行的绑定,使其Session保持一致。

4、F5 BIG-IP的iRules功能可以做HTTP内容过滤,根据不同的域名、URL,将访问请求传送到不同的服务器。


https://zhuanlan.zhihu.com/p/71825940https://www.haproxy.com/blog/layer-4-load-balancing-direct-server-return-mode/

应用场景

DNS 轮询算法,一个域名有多个IP解析 反向代理 nginx php-fpm master work方式

问题

1、对于负载均衡的后端服务,有两个节点A,B,在长链的情况下,他们的连接数都很平均,如果某个出现故障这个时候,如A 故障后,它的连接都转移到B上,这个时候A恢复,怎样平衡回去

这个问题如果是处理任务还可以通过work steal方式来处理这个问题,但是如果是长连接的话,需要在负载均衡上面通过修改最小连接或者加权来慢慢把B的连接追上,而不是轮询打到A上,因为此时A已经好高连接数了

长连接的优势

要点 跟短链接对比,避免重新协商 SSL,避免回话建立 考虑点 1、线路带宽 2、机器负载
解决方式 可以基于网络连接时间 与 每个机器的负载,取权重且计算出方差去挑选那个节点比较优
如 流媒体内容的质量仍然取决于用户的连接速

https://www.open-open.com/lib/view/open1426302583482.html

https://lushunjian.github.io/blog/2018/07/28/%E9%95%BF%E8%BF%9E%E6%8E%A5%E7%9A%84%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/


案例 

阿里巴巴为什么能抗住90秒100亿? https://zhuanlan.zhihu.com/p/338884995

亿级Web系统搭建 Web负载均衡的几种实现方式(阿里) https://www.cnblogs.com/aspirant/p/11607839.html

资料

论文 https://en.wikipedia.org/wiki/MetaheuristicHierarchical Work-Stealing https://hal.inria.fr/inria-00429624v2/documentOn the Scalability of Constraint Programming on Hierarchical Multiprocessor Systems https://dspace.uevora.pt/rdpc/bitstream/10174/10653/1/icpp2013.pdf
Scheduling Multithreaded Computations by Work Stealing http://supertech.csail.mit.edu/papers/steal.pdfTree-Shaped computation Tree Shaped Computations as a Model for Parallel Applications https://en.wikipedia.org/wiki/Distributed_tree_searchTree Shaped Computations as a Model for Parallel Applications San98b.pdf https://en.wikipedia.org/wiki/Distributed_tree_searchhttps://en.wikipedia.org/wiki/Round-robin_schedulinghttps://en.wikipedia.org/wiki/Message_passinghttps://en.wikipedia.org/wiki/Distributed_shared_memoryhttps://en.wikipedia.org/wiki/Distributed_cachehttps://en.wikipedia.org/wiki/Load_balancing_(computing)#Non-hierarchical_architecture,_without_knowledge_of_the_system:_work_stealing

http://www.uml.org.cn/zjjs/201807091.asp
https://zhuanlan.zhihu.com/p/31777732
Web后端系统架构漫谈(1)——负载均衡 https://nullcc.github.io/2017/11/23/Web%E5%90%8E%E7%AB%AF%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E6%BC%AB%E8%B0%88(1)%E2%80%94%E2%80%94%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1/
六大Web负载均衡原理与实现 https://www.cnblogs.com/aspirant/p/9087716.html
LVS集群中的IP负载均衡技术 https://cloud.tencent.com/developer/article/1031890https://www.cnblogs.com/aspirant/p/9084740.html
负载均衡的原理和实现细节 – 代码 https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/全局负载均衡GSLB https://jjayyyyyyy.github.io/2017/05/17/GSLB.htmlhttps://www.efficientip.com/what-is-gslb/

Mongodb 客户端怎么做负载均衡

mongoDB 客户端
mongoDB

问题

在mongodb的社群里面有人问 mongos 连接不均

于是乎我就看了一下源码分析

先提出疑问

1、mongodb 是怎样建立连接

2、怎样选择连接节点

3、集群或分片下是怎样选择节点

分析从一个dsn 新建链接的过程

下面是libmongoc源码
首先根据dsn字符创建对象 uri 结构体


uri = mongoc_uri_new_with_error (uri_string, &error); -> 解析这个dsn的语义

处理dsn的结构 uri信息
mongoc_client_new_from_uri 
分两个步骤

1、mongoc_topology_new -> 看这个mongo 是单体,副本,还是分片,连接协议,寻址服务器地址的say hello 连接时间,并且加入扫描队列定时更新
    mongoc_topology_scanner_new -> 调用 hello 命令 包含 所有节点的连接,最近写入,还有节点角色,一些写关注,读关注,超时时间参数等设置,形成服务列表

    https://docs.mongodb.com/manual/reference/command/hello/

    其中 mongoc_topology_description_add_server(使用快速排序), mongoc_topology_scanner_add(双向链表) 就是添加主机服务到扫描队列中

2、_mongoc_client_new_from_uri 根据 topology 信息,为客户端生成连接sock结构的client结构

客户端进行链接过程

当客户端进行操作的时候,所有命令发送都会建立session,源码部分位 _prime 函数 每个操作都会使用这个建立链接
    _prime->_mongoc_cursor_run_command->mongoc_client_start_session->_mongoc_topology_pop_server_session -> 找可用服务器地址     形成 client->client_sessions = session + ss 结构,所有建立链接的回话都会存放 client_sessions 通过 b tree 方式进行搜索避免冲突
    选择节点 mongoc_topology_description_suitable_servers 函数 会根据 服务的架构类型进行筛选 如单体直接返回,副本找主,分片找mongos,如果写操作找主,读操作找副本 通过所有服务器筛选出能够进行访问的候选节点

通过以下算法
     /* Ways to get here:
    *   - secondary read
    *   - secondary preferred read
    *   - primary_preferred and no primary read
    *   - sharded anything
    * Find the nearest, then select within the window */
    for (i = 0; i < data.candidates_len; i++) {
      if (candidates[i] &&
          (nearest == -1 || nearest > candidates[i]->round_trip_time_msec)) {
         nearest = candidates[i]->round_trip_time_msec;
      }
   }

   for (i = 0; i < data.candidates_len; i++) {
      if (candidates[i] && (candidates[i]->round_trip_time_msec <=
                            nearest + local_threshold_ms)) {
         _mongoc_array_append_val (set, candidates[i]);
      }
   }

   返回 候选服务列表后,就随机在里面找一个节点

   mongoc_topology_description_suitable_servers (
      &suitable_servers, optype, topology, read_pref, local_threshold_ms);
   if (suitable_servers.len != 0) {
      rand_n = _mongoc_rand_simple (&topology->rand_seed);
      sd = _mongoc_array_index (&suitable_servers,
                                mongoc_server_description_t *,
                                rand_n % suitable_servers.len);
   }

大概意思是,找到响应时间最小的节点,local_threshold_ms 意思默认超时时间     

问题是 round_trip_time_msec 时间是怎样来的?

_server_monitor_update_topology_description 通过这个数据 也就是通过 hello 命令 更新节点的 round_trip_time_msec 同时把超时的节点剔除

    这个 mongodb 开启了一个线程进行    mongoc_client_pool_new 创建线程池 给 mongoc_client_pool_pop 调用 ->_start_scanner_if_needed->_mongoc_topology_background_monitoring_start->_mongoc_topology_background_monitoring_reconcile->_background_monitor_reconcile_server_monitor -> mongoc_server_monitor_run_as_rtt

总结

客户端选择响应节点处理过程
根据say hello 的命令得到所有节点的信息,进行分门别类,形成【拓扑图】 如果是 副本,在dsn里面有设置偏向读副本则读副本,如果是写指令则进行 如果是分片,则会使用mongos,里面就会进行,根据  round_trip_time_msec 时间也就是响应时间在阀值范围内,形成的数组里面随机挑选一个进行响应

结论:总结上面的问题是很大可能是由于其它节点响应时间不稳定导致不平均,在候选数组里面,组合位 【1,2】,【1】,【1】的情况比较多 架构为分片结构,可能导致上面不均

ps:也看过官网的java-drive 都是能够使用响应时间在阀值范围的节点集合随机挑选可用节点

参考资料

mongodb+srv 如果连接地址是域名,会根据 DNS SRV 记录进行解析ip
TXT 记录能够把连接上的参数都设置在TXT记录上,然后通过DNS信息获取进行组装 https://docs.mongodb.com/manual/reference/connection-string/
换句话说就是DNS 负载均衡,做服务发现
DNS SRV是DNS记录中一种,用来指定服务地址。与常见的A记录、cname不同的是,SRV中除了记录服务器的地址,还记录了服务的端口,并且可以设置每个服务地址的优先级和权重。访问服务的时候,本地的DNS resolver从DNS服务器查询到一个地址列表,根据优先级和权重,从中选取一个地址作为本次请求的目标地址。 https://www.lijiaocn.com/%E6%8A%80%E5%B7%A7/2017/03/06/dns-srv.html

/*     * Set topology type from URI:     *   + if directConnection=true     *     – whether or not we have a replicaSet name, initialize to SINGLE     *     (directConnect with SRV or multiple hosts triggers a URI parse error)     *   + if directConnection=false     *     – if we’ve got a replicaSet name, initialize to RS_NO_PRIMARY     *     – otherwise, initialize to UNKNOWN     *   + if directConnection was not specified in the URI (old behavior)     *     – if we’ve got a replicaSet name, initialize to RS_NO_PRIMARY     *     – otherwise, if the seed list has a single host, initialize to SINGLE     *   – everything else gets initialized to UNKNOWN     */

session id 算法 _mongoc_server_session_uuid https://tools.ietf.org/html/rfc4122#page-14
随机的帖子: https://zhuanlan.zhihu.com/p/64538762
mongos 的选择 是通过客户端进行,其中这个算法是通过公共库
起算法为
https://docs.mongodb.com/manual/core/read-preference-mechanics/

Leetcode 刷题 第八天 – 617. 合并二叉树,116. 填充每个节点的下一个右侧节点指针 – 解题思路

617. 合并二叉树

解题思路:递归函数,不断把两个点相加,边界范围为他们在递归的下一个节点为空,称为上一个节点左或右

func mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode {
    if root1 == nil {
        return root2
    }

    if root2 == nil {
        return root1
    }

    root1.Val += root2.Val
    root1.Left = mergeTrees(root1.Left, root2.Left)
    root1.Right = mergeTrees(root1.Right, root2.Right)

    return root1
}

时间复杂度:O(min(m,n))O(min(m,n)),其中 mm 和 nn 分别是两个二叉树的节点个数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会对该节点进行显性合并操作,因此被访问到的节点数不会超过较小的二叉树的节点数。

空间复杂度:O(min(m,n))O(min(m,n)),其中 mm 和 nn 分别是两个二叉树的节点个数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。

116. 填充每个节点的下一个右侧节点指针

解题思路:这个是二叉树层次遍历

步骤如下

1、先父节点的左边开始,左节点连接右节点,

2、父节点的相邻节点在开始

3、重复1,2直到走完左节点

func connect(root *Node) *Node {
    if root == nil {
        return root
    }
    leftNode := root

    for leftNode.Left != nil {
        node := leftNode

        // 遍历层
        for node != nil {
            // 左右节点连接
            node.Left.Next = node.Right

            if node.Next != nil {
                node.Right.Next = node.Next.Left
            }
            
            node = node.Next
        }

        leftNode = leftNode.Left
    }

    return root
}
时间复杂度:O(N),每个节点只访问一次。
空间复杂度:O(1),不需要存储额外的节点。

Leetcode 刷题 第七天 – 733. 图像渲染,695. 岛屿的最大面积 解题思路

733. 图像渲染

解题思路:然每个节点进行广度或深度遍历,这里需要注意一个边界问题防止重复添加到访问,这个要点在把不同颜色判断为需要遍历的节点

func floodFill(image [][]int, sr int, sc int, newColor int) [][]int {
    lenc := len(image[0])
	lenr := len(image)

	if sr > lenr-1 || sc > lenc-1 {
		return image
	}

	centerColor := image[sr][sc]

	if centerColor == newColor {
		return image
	}

	// 上左下右顺序深度遍历
	image[sr][sc] = newColor

	// 记录遍历的点
	queue := [][]int{{sr, sc}}
	// fmt.Println(queue, image)
	// 从rc rs 出发
	for i := 0; i < len(queue); i++ {
		pos := queue[i]

		// 上
		if pos[0]-1 >= 0 && image[pos[0]-1][pos[1]] == centerColor {
			// fmt.Println("上")
			queue = append(queue, []int{pos[0] - 1, pos[1]})
			image[pos[0]-1][pos[1]] = newColor
		}
		// 右
		if pos[1]+1 < lenc && image[pos[0]][pos[1]+1] == centerColor {
			// fmt.Println("右")
			queue = append(queue, []int{pos[0], pos[1] + 1})
			image[pos[0]][pos[1]+1] = newColor
		}
		// 下
		if pos[0]+1 < lenr && image[pos[0]+1][pos[1]] == centerColor {
			// fmt.Println("下")
			queue = append(queue, []int{pos[0] + 1, pos[1]})
			image[pos[0]+1][pos[1]] = newColor
		}
		// 左
		if pos[1]-1 >= 0 && image[pos[0]][pos[1]-1] == centerColor {
			// fmt.Println("左")
			queue = append(queue, []int{pos[0], pos[1] - 1})
			image[pos[0]][pos[1]-1] = newColor
		}
		// fmt.Println(queue)
	}
	return image
}


时间复杂度:O(n x m)O(n×m),其中 nn 和 mm 分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

空间复杂度:O(n x m)O(n×m),其中 nn 和 mm 分别是二维数组的行数和列数。主要为队列的开销。

695. 岛屿的最大面积

解题思路:图的遍历都需要考虑一个是否访问过的问题,因此提也是通过对比值进行,把访问过的变位0,就不会在访问统计队列中

时间复杂度:O(n x m)O(n×m), 空间复杂度:O(n x m)O(n×m)

nn 和 mm 分别是二维数组的行数和列数

总结

解题思路,广度优先搜索 / 深度优先搜索,都是需要考虑边界问题,防止重复访问,需要遍历全部并且需要额外的存储空间

Leetcode 刷题 第六天 – 567. 字符串的排列

567. 字符串的排列

解题思路:此题,我没有看清楚,原来是需要找子串的排列组合在主串的是否存在

// 标准答案,通过26个字母位置统计总数,如果位置相等则保存说明子串与存在主串中
func checkInclusion(s1, s2 string) bool {
    n, m := len(s1), len(s2)
    if n > m {
        return false
    }
    var cnt1, cnt2 [26]int
    for i, ch := range s1 {
        cnt1[ch-'a']++
        cnt2[s2[i]-'a']++
    }
    if cnt1 == cnt2 {
        return true
    }
    for i := n; i < m; i++ {
        cnt2[s2[i]-'a']++
        cnt2[s2[i-n]-'a']--
        if cnt1 == cnt2 {
            return true
        }
    }
    return false
}


// 没有考虑到子串组合
func checkInclusion(s1 string, s2 string) bool {
s1Len := len(s1)
	s2Len := len(s2)

	maxLen := s1Len
	maxS := s1
	minLen := s2Len
	minS := s2
	if s2Len > s1Len {
		maxLen, minLen = minLen, maxLen
		maxS, minS = minS, maxS
	}
	// fmt.Println(maxS, minS)
	j := 0
	for i := 0; i < maxLen; i++ {
		// 正向匹配

		tmp := i
		j = 0
		for j < minLen && tmp < maxLen {
			// fmt.Println(tmp, j, minLen, string(maxS[tmp]), string(minS[j]))
			if maxS[tmp] == minS[j] {
				j++
				tmp++
			} else {
				j = 0
				break
			}
		}
		if j == minLen {
			return true
		}
		// 逆向匹配
		tmp = i
		j = 0
		for j < minLen && tmp < maxLen {
			// fmt.Println(tmp, j, minLen, maxS[tmp], minS[minLen-1-j], minLen-1-j)
			if maxS[tmp] == minS[minLen-1-j] {
				j++
				tmp++
			} else {
				j = 0
				break
			}
		}
		// fmt.Println(j)
		if j == minLen {
			return true
		}
	}

	return false
}

3. 无重复字符的最长子串

解题思路:这条题对比这个字符是否包含在字符串内,方式有快慢指针对比两个数组,滑动窗口,kmp算法,此题需要反转数组在比对,因此kmp不合适

 maxLen := 0
	sLen := len(s)

	for i := 0; i < sLen; i++ {
		j := i
		curHash := make(map[byte]int)
		// fmt.Println(i, j, sLen, s[j])
		for j < sLen && curHash[s[j]] == 0 {
			curHash[s[j]]++
			j++
		}
		// fmt.Println(curHash)
		if maxLen < j-i {
			maxLen = j - i
		}
	}

	return maxLen

Leetcode 刷题 第五天 – 876. 链表的中间结点,19. 删除链表的倒数第 N 个结点

876. 链表的中间结点

解题思路,双指针,快慢指针,快慢指针只差一个值

func middleNode(head *ListNode) *ListNode {
    slow := head;
    fast := head;
    for fast != nil && fast.Next != nil {
        slow = slow.Next;
        fast = fast.Next.Next;
    }
    return slow;
}

19. 删除链表的倒数第 N 个结点

解题思路:双指针,通过(长度 – 要删除的位置)的前n个元素的要删除的这个元素的前一个节点,根据前一个节点的后一个的后一个,为要删除的节点的后面元素得剩余数组

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    lenN := getNodesLen(head)
    // fmt.Println(lenN)
    dummy := &ListNode{0, head}
    cur := dummy
    for i:=0; i < lenN - n; i+=1 {
        cur = cur.Next
    } 
    // if cur.Next != nil {
        cur.Next = cur.Next.Next
    // } else {
    //     head = nil
    // }
    return dummy.Next
}

func getNodesLen(head *ListNode) int {
    n:=0
    for ; head !=nil; head = head.Next {
        n+=1
    }
    return n
}

Leetcode 刷题 第四天 – 344. 反转字符串,557. 反转字符串中的单词 III

344. 反转字符串

解题要点:设i,j作为指针分别指向头与尾,交换位置,实现原地算法

func reverseString(s []byte)  {
    j := len(s) - 1
	i := 0
	for i < j  {
		s[i], s[j] = s[j], s[i]
		i += 1
		j -= 1
	}
}

557. 反转字符串中的单词 III

解题要点:找出空格位置,然后根据这个位置找到这个单词进行反转

func reverseWords(s string) string {
    length := len(s)
	ret := []byte{}
	for i := 0; i < length; {
		start := i
		for i < length && s[i] != ' ' {
			i++
		}
		for p := start; p < i; p++ {
			ret = append(ret, s[start+i-1-p])
		}
		for i < length && s[i] == ' ' {
			i++
			ret = append(ret, ' ')
		}
	}
	return string(ret)
}
func reverseWords(s string) string {
	top := 0
	bottom := 0
	n := len(s)
	ret := []byte{}
	start := 0
	for top < n {
		if s[top] == ' ' {
			fmt.Println(top, bottom)
			for bottom < top {
				fmt.Println(top, bottom, start+top-bottom-1, string(s[start+top-bottom-1]))
				ret = append(ret, s[start+top-bottom-1])
				bottom += 1
			}
			ret = append(ret, ' ')
			bottom = top + 1
			start = bottom
			fmt.Println(string(ret), "----=", string(s[start+top-bottom-1]), top, bottom)
		}
		top += 1
	}

	top = n
	start = bottom
	for start < n {
		fmt.Println(bottom + top - start - 1)
		ret = append(ret, s[bottom+top-start-1])
		start += 1
	}
	return string(ret)
}

总结

1、字符串反转,用双指针是非常好的实现

2、原地算法,减少额外数组的使用

3、反转算法,使用原地算法进行字符串的反转操作,不需要额外的空间申请,通过双指针进行位置记录使得每次循环能够替换位置

各种消息队列设计要点与对比

定义

一个服务,数据队列,由生产者(发),消费者(收),队列(存)三者组成进行数据的先进先出的逻辑操作

优点

  • 提高响应,系统解耦,错峰与流控
  • 业务中让一些不是响应实时,不核心的操作,进行异步操作
  • 有序性

缺点

  • 数据更新不及时

Ps:然而说增加业务延迟,这个只是把时间转嫁到异步处理,所以要看场景应用,在用户端不用长久等待,可以在体验上增加一些loading机制,表示处理中,减轻延迟带来的问题,一是整个流程都在等待,一是增加交互特性进行过度

消息模式

队列模式(单播,点到点)

生成者生产消息插入队列,在由消费者进行队列的对头消息进行接口消费,处理完就删除该队列消息,也就是出队操作

发布-订阅模式(广播)

把队列设置为主题模式,也就是分队列类型,由对应的消费者进行消费(订阅),同一个消息可以进行多个消费者进行订阅

与队列模式的区别为,发布-订阅同一条消息能够被多个消费者订阅,当发布-订阅只有一个订阅者的时候,就跟队列模式一样

kafka消息模式

生产者-》消息队列-》广播 消费A,消费B

问题:消费A所分发的消息不是它想要的

生产者A,B-》topic routing-》让消费A订阅生成A,B订阅B

问题:如何解决多个消费者对同一个Topic

生产者A,B-》topic routing-》topic1 复制一个 topic2分别让消费A,A1 订阅消费

问题:成本高,重复消费,需要消费者排重

生产者A,B-》topic routing-》topic1 分别让消费A,A1 使用偏移量进行订阅主题,每个环节的信息offset-A offset-A1

把问题都转嫁给消费者,变成一个存储系统

https://zhuanlan.zhihu.com/p/367704356

应用场景

系统解耦

异步通信

流量削峰

延迟通知

最终一致性保证

顺序消息

流式处理

业务场景例子

一个业务完成后,需要进行多个子系统(相互独立)的内容更新,由串行变为异步

通过系统解耦,解决一个业务运行的串行带来的时延与相互影响,提高吞吐量

架构设计

角色

Broker 服务端,MQ核心,提供接口给生产者和消费者,负责消息增删改查

Producer 生产者

Consumer 消费者

设计难点

1、RPC 通信,消费者生产者,自动注册到这个MQ上

2、高可用,Broker需要保证水平扩展,童工服务自动注册与肺癌安,负载均衡,超市重试机制,发送和消费消息通过ack机制来保证

方案:1、kafka分区+多副本,2、db,分布式文件系统,带持久化KV系统

3、存储,追加写日志+索引文件,查找消息利用跳转表,二分查找,通过操作系统的页缓存、零拷贝提升磁盘文件的读写性能

4、高性能,Reactor网络IO默写,业务线程池设计,生产端批量发送、Broker 端异步刷盘,消费端批量拉取

消息队列设计

RPC 通信协议

服务端提供两个RPC服务,一个用来接收消息,一个用来确认消息收到,中间可能还涉及跨IDC的服务的问题。这里和RPC的原则是一致的,尽量优先选择本机房投递

高可用/高扩展

服务自动发现,负载均衡等功能,保证broker接受消息和确认消息的接口是幂等broker多机器共享一个DB或者一个分布式文件/kv系统实现

消息堆积的能力

为了满足错峰/流控/最终可达把消息存储下来,然后选择时机投递,主要有持久化和非持久化两种。 持久化的形式能更大程度地保证消息的可靠性,很多消息对于投递性能的要求大于可靠性的要求,且数量极大(如日志)。这时候,消息不落地直接暂存内存,尝试几次failover,最终投递出去也未尝不可

存储选型

从速度来看,文件系统>分布式KV(持久化)>分布式文件系统>数据库,而可靠性却截然相反,消息队列是用来支持支付/交易等对可靠性要求非常高,但对性能和量的要求没有这么高,而且没有时间精力专门做文件存储系统的研究,DB是最好的选择

消息关系处理

解析发送接收关系,进行正确的消息投递了,组间广播、组内单播,消息需要通知到多个业务集群,而一个业务集群内有很多台机器,消费关系除了组内组间,可能会有多级树状关系,一般比较通用的设计是支持组间广播,不同的组注册不同的订阅。组内的不同机器,如果注册一个相同的ID,则单播;如果注册不同的ID(如IP地址+端口),则广播。 至于广播关系的维护,一般由于消息队列本身都是集群,所以都维护在公共存储上,如config server、zookeeper等。维护广播关系所要做的事情基本是一致的: 发送关系的维护。 发送关系变更时的通知。

最终一致性

当失败或者不知道成功失败(比如超时)时,消息状态是待发送,定时任务不停轮询所有待发送消息,最终一定可以送达,过程为

producer往broker发送消息之前,需要做一次落地。

请求到server后,server确保数据落地后再告诉客户端发送成功。

支持广播的消息队列需要对每个待发送的endpoint,持久化一个发送状态,直到所有endpoint状态都OK才可删除消息。

数据去重处理

消费确认,允许消费方主动ack。 对于正确消费ack的

顺序消息、重复消息

允许消息丢失。从发送方到服务方到接受者都是单点单线程。

每一个消息应该有它的唯一身份。不管是业务方自定义的,还是根据IP/PID/时间戳生成的MessageId,如果有地方记录这个MessageId,消息到来是能够进行比对就 能完成重复的鉴定,实现方式数据库的唯一键/bloom filter/分布式KV中的

如何鉴别消息重复,并幂等的处理重复消息。一个消息队列如何尽量减少重复消息的投递。

0、ID判断

1、版本号

解决重复问题,需要在接收时候比对版本是否大于当前版本

解决顺序问题,乱序消息会暂存消息,待先处理小版本号在处理大的版本号次序化

问题

1、对发送方必须要求消息带业务版本号

2、下游必须存储消息的版本号,对于要严格保证顺序的,所有节点都存储消息成本高

3、状态机,状态有上线/下线状态

消费者只需要把“我不能处理这个消息”告诉投递者,要求投递者过一段时间重发即可。而且重发一定要有次数限制

假设产品本身状态是下线,1是上线消息,2是下线消息,3是上线消息,正常情况下,消息应该的到来顺序是123,但实际情况下收到的消息状态变成了3123。 那么下游收到3消息的时候,判断状态机流转是下线->上线,可以接收消息。然后收到消息1,发现是上线->上线,拒绝接收,要求重发。然后收到消息2,状态是上线->下线,于是接收这个消息。 此时无论重发的消息1或者3到来,还是可以接收。另外的重发,在一定次数拒绝后停止重发,业务正确

重复消息的处理

由消费方保证的,我们要做的是减少消息发送的重复

减少重复消息的关键步骤:

1、broker记录MessageId,直到投递成功后清除,重复的ID到来不做处理,这样只要发送者在清除周期内能够感知到消息投递成功,就基本不会在server端产生重复消息。

2、对于server投递到consumer的消息,由于不确定对端是在处理过程中还是消息发送丢失的情况下,有必要记录下投递的IP地址。决定重发之前询问这个IP,消息处理成功了吗?如果询问无果,再重发。

事务处理

两阶段提交,分布式事务。本地事务,本地落地,补偿发送。

分布式事务一定构建与比较靠谱的商用DB和商用中间件上,成本也太高

说明

以本地和业务在一个数据库实例中建表为例子,与扣钱的业务操作同一个事务里,将消息插入本地数据库。如果消息入库失败,则业务回滚;如果消息入库成功,事务提交

问题

配置较为复杂,“绑架”业务方,必须本地数据库实例提供一个库表。

对于消息延迟高敏感的业务不适用。

如,强事务,保证扣钱加钱

一个完整的消息队列应该定义清楚自己可以投递的消息类型,如事务型消息,本地非持久型消息,以及服务端不落地的非可靠消息等

异步与性能

任何的RPC都是存在客户端异步与服务端异步的,任意组合的:客户端同步对服务端异步,客户端异步对服务端异步,客户端同步对服务端同步,客户端异步对服务端同步

准确地说是客户端半同步半异步(使用线程池不阻塞主流程,但线程池中的任务需要等待server端的返回),server端是纯异步。客户端的线程池wait在server端吐回的future上,直到server端处理完毕,才解除阻塞继续进行

同步能够保证结果,异步能够保证效率,要合理的结合才能做到最好的效率

批量处理

消费者到底应该何时进行消费。大处着眼来看,消费动作都是事件驱动的

攒够了一定数量。

到达了一定时间。消息延迟

队列里有新的数据到来。及时性要求高的数据

push还是pull

pull模型,如Kafka、MetaQ,consumer可以按需消费,不用担心自己处理不了的消息来骚扰自己,而broker堆积消息也会相对简单,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于建立索引等慢消费,消息量有限且到来的速度不均匀的情况

push模型,慢消费最大的致命伤,消费者的速度比发送者的速度慢很多,势必造成消息在broker的堆积,消息就要一直在broker端保存,最致命的是broker给consumer推送一堆consumer无法处理的消息,consumer不是reject就是error

慢消费,解决用pull模式,push等于攻击消费者

消息延迟与忙等

pull模式的问题,主动权在消费方,消费方无法准确地决定何时去拉取最新的消息,时间不确定,1分钟内连续来了1000条消息,然后半个小时没有新消息产生, 可能你的算法算出下次最有可能到来的时间点是31分钟之后,或者60分钟之后,结果下条消息10分钟后到了,假设40ms到80ms之间的50ms消息到来,消息就延迟了30ms,而且对于半个小时来一次的消息,这些开销就是白白浪费的

一种优化的做法-长轮询,来平衡推拉模型各自的缺点,费者如果尝试拉取失败,不是直接return,而是把连接挂在那里wait,服务端如果有新的消息到来,把连接notify起来,这也是不错的思路。但海量的长连接block对系统的开销还是不容小觑的,还是要合理的评估时间间隔,给wait加一个时间上限比较好~

顺序消息

push模式的消息队列,支持分区,单分区只支持一个消费者消费,并且消费者只有确认一个消息消费后才能push送另外一个消息,还要发送者保证全局顺序唯一

pull模式,如果想做到全局顺序消息,就相对容易很多:

producer对应partition,并且单线程。

consumer对应partition,消费确认(或批量确认),继续消费即可

对于日志push送这种最好全局有序,允许出现小误差的场景,pull模式非常合适。如果你不想看到通篇乱套的日志

顺序消息的场景还是比较有限的而且成本太高

工具比较

说起消息队列,ActiveMQ、RabbitMQ、RocketMQ、Kafka、Pulsar,ZeroMQ,Redis

组件RocketMQRabbitMQActiveMQKafkaRedisZeroMQPulsar支持云原生,发展潜力大
协议支持Tcp,JMS,openMeesaging支持AMQP,XMPP,SMTP,STOMP支持AMQP MQTTJMS协议支持Tcp接入没有什么队列协议,支持订阅模式TCP、UDP、IPC、广播TCP
性能10万条/秒万级万级单机写入 TPS 号称在百万条/秒万级万级万亿,计算与存储分离,易扩展
可靠性(消息丢失)支持异步/同步刷盘;异步/同步Replication支持磁盘经过消息确认、持久化等手段保证,支持内存,文件较低概率丢失数据,支持内存,文件,数据库异步刷盘方式,异步Replication较低概率丢失数据内存型,提供aof,rdb两种但是还是会丢失有多种可靠性,请求回应方式https://zguide.zeromq.org/docs/chapter4/通过broker分布式存储
时效性支持push模型,pull模型支持push模型,pull模型Push 模式毫秒级pull 模式支持pull长轮询pull模式1:MPull 与 push 模式支持 N:MPull 与 push 模式支持 N:M
持久化高性能和低延迟的文件存储同步刷盘 异步刷盘高性能文件存储需要外带高性能日志系统,如leveldb通过磁盘顺序读写与零拷贝机制aof和rdb两种机制进行持久化,但还是会丢失(pub/sub) 有个缺点就是消息无法持久化Stream。它提供了消息的持久化和主备复制功能支持,在消息发送端保存https://zguide.zeromq.org/docs/chapter4/#Disconnected-Reliability-Titanic-Pattern通过broker分布式存储,raft 协议同步数据,也就是过半成功才返回ack给生产者
队列数单机支持最高5万个队列,性能稳定5w个队列,单机单线程https://www.cloudamqp.com/blog/part1-rabbitmq-best-practice.html单机超过64个队列/分区,消息发送性能降低严重本来是通过自身数据结构存储,看内存
事务支持,通过半消息实现(暂存到mqserver,不提供给消费者,生产者的本地事务全完成才给消费者,否则把消息丢弃)https://rocketmq.apache.org/rocketmq/the-design-of-transactional-message/https://help.aliyun.com/document_detail/112010.html?spm=a2c4g.11186623.6.564.49ff2d67ZYvxCl事务机制有关的方法有三个:txSelect(), txCommit()以及txRollback()支持不支持支持不支持支持https://pulsar.apache.org/docs/en/transactions/
死信支持支持专门有一个dead letter exchange进行存储支持不支持不支持,需要额外开发不支持支持
批量发送支持,1024条,设定缓存条数,时间进行批量消费不支持不支持支持,带有异步生产者不支持支持,订阅模式1:N N:M, 它会进可能发送所有消息http://wiki.zeromq.org/area:faq支持
消息有序性严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序保证独立分区消息的顺序独立消费者、独立队列保证消费有序保证独立分区内消费的顺序,但一台Broker down机,机会产生消息乱序Pipelining一个消费者不支持支持,通过分区实现,使用 SinglePartition或者RoundRobinPartition模式
延迟推送支持可支持,通过TTL设置消息到死信队列中进行消费https://www.cnblogs.com/mfrank/p/11260355.html支持NMS schedule不支持能支持blpop/brpop,需要在消费端实现,但是这样会让消费端阻塞本身不支持,需要额外实现支持
广播消息支持集群消费:当使用集群消费模式时,消息队列RocketMQ版认为任意一条消息只需要被集群内的任意一个消费者处理即可。广播消费:当使用广播消费模式时,消息队列RocketMQ版会将每条消息推送给集群内所有注册过的消费者,保证消息至少被每个消费者消费一次。支持支持不支持支持支持,Push-Pull模型实现支持,其实就是订阅多个消费者,使用push方式
消息过滤支持不支持,但可以自行封装。支持支持,使用 streams过滤消息支持,streamed message不支持支持
消息查询根据message id支持Producer客户端设置Message ID属性,为每条消息设置唯一标识符只能针对主题,额外用命令过滤查询不支持Redis streams支持不支持支持,支持sqlhttps://pulsar.apache.org/docs/zh-CN/next/sql-getting-started/
消息回溯支持,支持按照时间回溯不支持。RabbitMQ中消息一旦被确认消费就会被标记删除不支持支持,offset查找,但确定不了那个消费者,那个生产者生成,消费不支持不支持
消息优先支持支持支持支持,按offset进行支持,需要在消费端实现不支持支持
消息重发支持,重试间隔时间顺延不支持不支持支持不支持不支持支持
消息积压(消费不来)能够持久化支持支持支持topic下消费者较长时间离线,消息堆积量大支持不支持
消息确认支持支持支持支持支持,维护两个队列:pending队列和doing表支持支持,消费者ack确认,给broker,从而可删除消息体
信息轨道追踪(生产者,消费者,队列的信息状态)https://help.aliyun.com/document_detail/43357.html可以通过Message ID、Message Key或Topic的时间范围查询相关的消息轨迹,找到消息的实际收发状态,帮助诊断问题支持不支持不支持不支持不支持不支持
API完备性SDK支持丰富SDK支持丰富SDK支持丰富JMS版本多SDK丰富,大多支持TCPSDK丰富SDK丰富SDK丰富
分布式系统高可用非常高(分布式架构)集群模式主从,取决于存储,如果是用kahadb ,需要zookeepr支持分布式架构Broker、Producer、Consumer都原生自动支持分布式,集群,哨兵,主从去中心化,不支持集群非常可靠,分布式系统
消息分发round-robin轮询机制,轮询消费者round-robinprefetchCount,消费者来不及处理,消息会堆积在队列中,新启动的消费者可以马上从队列中取到消息开始工作轮询(默认)或按照严格顺序依赖zookeeper自动实现复杂均衡zookeeper管理集群中的broker sonsumer,通过zookeeper的协调机制,producer会记录topic对应的broker,对broker进行轮询或者随机访问broker轮询机制只能一个topic 一个consumer接收端负载均衡支持轮询机制sharding机制
管理界面、监控指标支持web管理工具丰富支持web和终端命令支持web和终端命令运维工具丰富支持,使用一般支持,终端命令有丰富的运维指令,需要额外按照非官方的管理工具ZMQ 协议开放监控内容,需要自己开发https://blog.csdn.net/yaomingyang/article/details/101380723支持web管理工具丰富支持web和终端命令https://pulsar.apache.org/docs/zh-CN/next/admin-api-overview/
社区支持、文档完备阿里与apache社区支持完整成熟,社区活跃高,现在维护越来越少完善完善完善,而且国际化
权限控制用户方面:ACL特性服务方面:所有服务组件API级别的鉴权支持SSL、SASL身份认证和读写权限控制。用户验证队列增删改授权支持SSL、SASL身份认证和读写权限控制单一,只有用户验证多租户系统设计用户与服务都支持鉴权
热启动实时修改参数不用重启节点,直接reload实时加载updateBrokerConfig更新 broker信息,不用重启而且能够在nameserv 上进行分发rabbitmqctl eval大部分参数能够变更如果需要升级整个node需要红绿部署支持runtimeConfigurationPlugin checkPeriod=”1000″支持动态修改Dynamic Update Mode通过zookeeper 同步所有broker支持不支持
流量控制基于Credit-Based算法,是内部被动触发的保护机制,作用于生产者层面。异步发送如果超过内存阀值,也就是活动窗口 ProducerWindowSize 就会进行阻塞https://activemq.apache.org/producer-flow-control支持client和user级别,通过主动设置可将流控作用于生产者或消费者。需要自身实现不支持多租户,能够适当低进行磁盘配额、流控制、限流调节
多租户开源版本没有支持Virtual Hosts不支持没有不支持不支持支持
冷热数据支持,分层存储,当 backlog 大小到达如10m,也可以精细到topichttps://pulsar.apache.org/docs/en/concepts-tiered-storage/
特点场景异步解耦秒杀或团队抢购活动(削峰填谷)大规模机器的缓存同步分布式事务的数据一致性https://help.aliyun.com/document_detail/112010.html?spm=a2c4g.11186623.6.564.49ff2d67ZYvxClErlang 并发性能好功能齐全日志处理基本上都需要自身实现队列的所有事情基本上 Redis Streams 是比较容易实现消息队列支持高并发的异步 Socket 框架支持的通讯模型适用于大型集群和分布式计算多线程去锁化定位只是一个多线程网络库场景分布式任务分发,如游戏加机saltstack的底层就使用了ZeroMQ作为通信机制Mongrel2是使用ZeroMQ开发的一个Web服务器计算与存储分开易水平扩展多租户低延迟多地理集群复制基本上所有队列的特性都包含并且提供高可用的解决方案

队列协议解析

协议就是针对某个功能特定定义好一组结构,如邮件,发送人,接收人,内容,附件等位置标记,便于客户端使用者解析

而队列协议基本结构是以publish broker subscribe 的三者关系进行定义

底层协议TCP,定义好结果

发布-订阅,MQTT,STOMP,WAMP

MQTT(Message Queue Telemerty Transport)是一种二进制协议,主要用于服务器和那些低功耗的物联网设备(IoT)之间的通信

https://developer.ibm.com/articles/iot-mqtt-why-good-for-iot/

STOMP 面向流文本的消息传输协议,WebSocket 通信标准。在通常的发布-订阅语义之上,它通过 begin/publish/commit 序列以及 acknowledgement 机制来提供消息可靠投递。

对于 WebSocket 来说,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。WebSocket是一个完整的应用层协议,包含一套标准的 API 。STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议可以建立在WebSocket之上,也可以建立在其他应用层协议之上。

WAMP , Web 应用消息协议,基于文本的协议标准,并且结合了基于发布-订阅的请求/响应编程模型,定义好,

两种消息传递模式: Publish & Subscribe(发布订阅) routed Remote Procedure Calls (rRPC)(路由远程过程调用)

https://juejin.cn/post/6844903923363348487

https://wamp-proto.org/index.html

队列,AMQP,XMPP,JMS

AMQP模型包括一套用于路由和存储消息的功能模块,以及一套在这些模块之间交换消息的规则。是一个二进制协议,拥有一些现代特点:多信道、协商式、异步、安全、跨平台、中立、高效

存储转发(多个消息发送者,单个消息接收者)。分布式事务(多个消息发送者,多个消息接收者)。

发布订阅(多个消息发送者,多个消息接收者)。基于内容的路由(多个消息发送者,多个消息接收者)。

文件传输队列(多个消息发送者,多个消息接收者)。点对点连接(单个消息发送者,单个消息接收者)。

结构

1.5. 约定 1.5.1. 定义 1.5.2. 版本号

https://baike.baidu.com/item/AMQP

https://zhuanlan.zhihu.com/p/147675691

XMPP(可扩展消息与存在协议) 传输的是与即时通讯相关的指令,定义了三个角色,客户端,服务器,网关。通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能

https://baike.baidu.com/item/XMPP/3430617

JSM (Java Messaging Service)是Java平台上有关面向消息中间件(MOM)的技术规范,于在两个应用程序之间,或分布式系统中发送消息,进行异步通信

支持两种模型:点对点或队列模型发布者/订阅者模型

https://baike.baidu.com/item/JMS/2836691

监控指标

1、主题数,订阅数,消费者,生成者,集群情况

2、发送速度,来掌握主题的流量情况

3、发送变化率,5 分钟内发送速率陡增了 2 倍

4、发送耗时,采样区间,[0, 1), [1, 5), [5, 10), [10, 50)

5、消息大小,采样区间,[0, 1), [1, 5), [5, 10), [10, 50)

6、日月周消息量

7、消费速度

8、消费积压

9、消费耗时

报警机制

发送 Tps 变化率 =(最大值 – 最小值)/中位数。5 分钟的 TPS 变化率为 3%。可以定时调度计算该指标,超过阈值(例如:100%)可以发送告警信息

资料

消息中间件:为什么我们选择 RocketMQ

一种低延迟的超时中心实现方式

高并发系列:架构优化之从BAT实际案例看消息中间件的妙用

万字长文:选 Redis 还是 MQ,终于说明白了!

https://mp.weixin.qq.com/s/K4xZvLU1pEp9d1m3hzbfFw

《吃透 MQ 系列》之扒开 Kafka 的神秘面纱

https://mp.weixin.qq.com/s/vSJCutIDHdP5AGmbAs13bA

高并发系列:架构优化之从BAT实际案例看消息中间件的妙用

https://mp.weixin.qq.com/s?__biz=MzA4ODUzMDg5NQ==&mid=2650001031&idx=1&sn=75b0eea86788b7b59c61875745b38c4c&scene=21#wechat_redirect

Facebook有序队列服务设计原理和高性能浅析

https://mp.weixin.qq.com/s?__biz=MzA4ODUzMDg5NQ==&mid=2650000874&idx=1&sn=8b35ff5f06d78edef7ea8fbbac8ab5a6&scene=21#wechat_redirect

Redis实现消息队列的4种方案

https://www.jianshu.com/p/d32b16f12f09

消息队列设计精要

https://tech.meituan.com/2016/07/01/mq-design.html

MQ案例场景处理

http://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/RocketMQ%20%E5%AE%9E%E6%88%98%E4%B8%8E%E8%BF%9B%E9%98%B6%EF%BC%88%E5%AE%8C%EF%BC%89/20%20RocketMQ%20%E9%9B%86%E7%BE%A4%E7%9B%91%E6%8E%A7%EF%BC%88%E4%BA%8C%EF%BC%89.md

RocketMQ学习之安装部署及基础讲解

https://www.cnblogs.com/jing99/p/13166602.html

RocketMQ幂等性顺序性实战, 及消息积压解决方案

https://www.cnblogs.com/wlwl/p/10668197.html

https://zhuanlan.zhihu.com/p/363211923

ZeroMQ

https://wizardforcel.gitbooks.io/zmq-guide/content/chapter2.html

https://zguide.zeromq.org/docs/chapter1/

https://learning-0mq-with-pyzmq.readthedocs.io/en/latest/pyzmq/basics.html

ZeroMQ简介及应用场景分析

https://blog.csdn.net/mysunshinexia01/article/details/80871694

业界消息总线技术分析-ZeroMQ

https://bbs.huaweicloud.com/blogs/104842

zeromq源码分析笔记之架构(1)

https://www.cnblogs.com/zengzy/p/5122634.html

分布式消息队列差异化总结,太全了!

https://cloud.tencent.com/developer/article/1469110

深入理解计算机 – 第一章为什么系列与要点

计算机 是怎样运行的
计算机的组成由 总线,IO设备,主存,处理器组成
其中CPU 通过对【寄存器】和【算数逻辑单元ALU】对数据和地址进行 加载,存储,操作,跳转对寄存器覆盖,存储,计算结果,调到某个寄存器进行数据处理

为什么计算机需要使用二进制
电子原件 高电压,低电压,更稳定的获取,高位1,低为0,这个时候需要通过一套二进制编码进行各种的16进制,10进制转化

为什么高速缓存至关重要
因为 磁盘 < 主存 < 高速缓存器 < 寄存器 依次递减且越接近CPU速度越快费用越高(精密制作) 这里音声了高速缓存的局部性原理,即常访问的数据和代码
为什么需要操作系统 更好管理 硬件资源,从而是进程更好使用硬件资源,抽象出一个标准出来,并且防止硬件被应用程序滥用

为什么需要进程
程序是一段字符,通过一定的语法形式,程序逻辑,通过编译器,编译对应平台的软件,软件在操作系统上运行称为程序
这个程序运行就是按照功能逻辑进行数据、显示、存储等操作的一个进程

进程是怎样运行
应用程序产生的进程,通过操作系统开放的系统调用,进行用户与内核态的切换,读取数据到CPU进行运算
为什么需要虚拟内存
给每个进程,独占主存的假象,因为内存是有限的,甚至无法把整个程序或数据都加载到内存里面,这个时候每个进程就会形成一个虚拟地址空间,这里由 程序代码和数据 堆,用户在运行时动态分配内存 共享库,与多个动态库,共享代码与数据的 栈,用于调用函数
为什么需要网络通信
能够超越地域空间进行数据交换
Amdahl 定理,标识对提升性能所带来的效果

Told 当前速度,k 为 部分性能提升的比咧,aTold 初始化所需时间,现在所需的时间 (aTold)/k Tnew = (1-a)Told + (aTold)/k = Told[(1-a)+ak
计算加速比 S =Told / Tnew =  1/(1-a) + a/k
意思说 想要加速整个系统,必须提升全系统中相当大的部分速度,当k无限大的时候 S = 1/(1-a) 标识

并发与并行
并发:同一时刻只运行一个任务,有多个可能在等待执行
并行:同时运行多个任务,同时执行
线程级并发,一个进程执行多个控制流
指令级并行,流水线执行,也就是同一个CPU运行时钟中,不同阶段的硬件加载不同任务的数据
单指令、多数据并行,处理器特殊硬件,运行一条指令产生多个并行执行操作

参考资料
https://www.cs.cmu.edu/~213/
在线课程 代码,ptf http://www.cs.cmu.edu/afs/cs/academic/class/15213-m20/www/schedule.html
学习资料 http://csapp.cs.cmu.edu/3e/students.html
练习题 http://csapp.cs.cmu.edu/3e/labs.html
深入理解计算机图 http://csapp.cs.cmu.edu/3e/figures.html
unix学习 http://heather.cs.ucdavis.edu/~matloff/unix.html
代码 http://csapp.cs.cmu.edu/3e/code.html
Linux/UNIX系统编程手册 pdf https://github.com/flycrane/kevin/blob/master/The%20Linux%20Programming%20Interface/Linux%E7%BC%96%E7%A8%8B%E6%8E%A5%E5%8F%A3%20-%20Linux%E5%92%8CUNIX%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B%E6%89%8B%E5%86%8C.pdf
Guide to GDB http://beej.us/guide/bggdb/