算法相关

260

算法是解决问题的一系列步骤,使用复杂度来衡量算法的效率。时间复杂度(time complexity)就是指令执行的次数,也就是执行消耗的时间。空间复杂度(space complexity)就是消耗存储空间。

创新思维: 1、用5升、7升桶获取6升水。首先,用7升桶装满水,倒入5升桶,剩余2升。5升倒掉,2升倒入5升桶。然后,7升桶装满水,倒入5升倒满,7升桶剩余4升水。5升倒掉,4升倒入5升桶。最后,7升桶装满,倒满5升,7升桶剩余6升水。

使用大O表示法估算时间复杂度:忽略常数、系数、低阶。常数估算为O(1);n+3估算为O(n);n^2+n估算为O(n^2); n^3+n^2估算为O(n^3);对数估算为O(logn);n+nlogn估算为O(nlogn);2^n估算为O(2^n)。 常见的复杂度比较:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)。n代表数据规模。多个数据规模 O(n+k)。一般情况下,均摊复杂度等于最好复杂度。 常见的时间复杂度:递归复杂度是O(2^n),类似二叉树,1+2+4+8+….=2^0+2^1+2^2+2^3+…+2^n=2^(n+1)-1 => O(2^n); 求第N个斐波那契数迭代法复杂度为O(n),一个for循环;使用数学公式复杂度为O(1);while ((n=n/2) > 0) 执行次数log2^n;

(数学计算:logn是以10为底n的对数,log2^n = log2^9 * log9^n,所以log2^n、log9^n都转换为:常数*logn;) (常见的执行次数计算:while ((n=n/2) > 0) {} ,除了多少次2,执行次数=log2^n, log 以2为底n的对数,如果是除以5,就是以5为底。2的多少次方等于n,就是以2为底n的对数,O(logn)。)

递归: 是为了简化解决问题的思路,让代码更加简洁,而不是为了求得最优解。 思想:拆解问题,大规模问题拆解为小规模问题,小规模问题变成更小规模问题。很多链表、二叉树相关的问题可以使用递归。 套路:先明确函数的功能,明确原问题和子问题的关系,。 递归调用的空间复杂度 = 递归深度 * 每次调用所需要的辅助空间。 二叉树遍历:空间复杂度就是O(h),最坏情况,就是链表h=n,O(n)。

求和公式:1+2+3+……+n = n*(1+n) / 2 : func sum(n int) { if n<=1 return n Return n + sum(n-1) }

func sum(n int) { if n<=1 return n return (n + 1) * n >> 2 }

求第N个斐波那契数 0 1 1 2 3 5 8 13….

算法一使用递归:
n<=1 return n;
fib(n) = fib(n-1)+ fib(n-2);
return fib(n);
复杂度 1+2+4+8+….=2^0+2^1+…….+2^n = 2^(n+1)-1=>O(2^n),二叉树,后面是前面的两倍。
算法二使用迭代法:
first := 0; second := 1;
for I:=0; I<n-1; I++ {
sum := first + second; first = second; second = sum;
} return second;。
算法三使用数学公式实现复杂度O(1)。

递归会出现很多重复计算,如何避免重复计算:可以使用数组来进行优化,存储index的计算,每个index只计算一次。时间复杂度可以优化到O(n),空间复杂度还是O(n),递归的深度*O(1)=O(n)。

func fib(n int) int {
//迭代
if n == 0 {
return 0
}
first := 0 second := 1 for i:=1; i<n; i++ {
sum := first + second
first = second
second = sum
}
return second
}

// 递归 func fib1(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}

动态数组:会造成内存空间大量浪费,可以使用缩容技术解决,缩容比例设置不当会导致复杂度震荡,但开辟、销毁内存空间的次数相对较少;扩容问题,底层数组空间不足,需要对底层数组扩容;删除、插入元素会出现元素内存挪动;清空数组要把所有对象元素置为nil;元素内存地址是连续的。

扩容:底层数组容量已经存满,需要重新创建一个新的更大容量的数组,把之前数组的元素挪动到新数组中,扩容可以是旧容量的1.5倍,即:oldCap + (oldCap >> 1),右移1就是0.5倍。X/2等价于x>>1。查看a的第b个数值为多少,(a>>b)&1。

缩容:如果内存使用比较紧张,动态数组有比较多的剩余空间,可以考虑进行缩容操作。如果扩容倍数、缩容时机设计不得当,有可能会导致复杂度震荡。

(动态数组接口:clear、size、isEmpty、contains(e)、add(e)、get(index)、set(index, e)、add(index, e)、remove(index)、indexOf(e)、arrayList(capacity)。)

NSMutableArray 底层包含:_used计数、_list缓冲区指针、_size缓冲区的大小、_offset是缓冲区的数组的第一个元素索引。__NSArrayM 使用了环形缓冲区 circular buffer。在头尾两端插入或删除不用移动内存;访问使用index+offset来获取值;在中间插入,会根据最少移动内存的方式插入;删除中间元素也是会最少移动内存的方式移动内存;删除头部或尾部元素不会进行内存移动;环形缓冲区满了就会进行扩容。使用链表来实现环形缓冲区比较好,扩容的时候不需要进行大量的内存移动,使用数组的话扩容的时候就需要大量内存移动。

__CFDictionary 结构体:内部有_keys、_values分别存储keys和values两个数组,数组的长度一致。根据key计算出哈希值h,空箱子数组容量n,计算index=h%n。hash冲突使用拉链法或寻址法解决。哈希表扩容,需要重哈希rehash。

1、原地删除有序数组中的重复项:给你一个有序数组nums,原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。不要使用额外的数组空间,必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。 利用双指针。判断len(nums)是否为0。初始化pre指针pre=0,for循环遍历数组元素,判断nums[pre] != nums[i], pre++ , 就交换:nums[pre], nums[i] = nums[I], nums[pre]。遍历结束,返回长度 pre+1。

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

left := 0
for right:=0; right<len(nums); right++ {
    if nums[left] != nums[right] {
        left++
        nums[left] = nums[right]
    }
}
return  left+1

}

删除有序数组中的重复项II:原地,每个元素最多出现两次。

func removeDuplicates(nums []int) int {
pre := 0
for i:=0; i<len(nums); i++ {
if pre < 2 || nums[pre-2] != nums[i] {
nums[pre] = nums[i]
pre++
}
}
return pre
}

2、整数数组nums,判断是否存在重复元素。存在就返回true。 借助字典map。初始化字典dict := make(map[int]int),遍历nums,判断字典中是否包含nums[I]为key的元素,_, ok := dict[nums[k]],如果没有就nums[i]为key,i为value存储到字典dict中。如果有返回true,就代表有重复。最后也没有查找到就返回false。

func containsDuplicate(nums []int) bool {
dict := make(map[int]int)
for i:=0; i<len(nums); i++ {
_, ok := dict[nums[i]]
if ok {
return true
} dict[nums[i]] = i
}
return false
}

借助set集合,把数组元素存储到set集合中,如果有重复,会被覆盖。最后比较set的长度和nums的长度是否相等,如果不等就有重复。

存在重复元素:给定一个整数数组和一个整数k,判断数组中是否存在两个不同的索引i和j,使得nums[i] = nums[j],并且i和j差的绝对值至多为k。

func containsNearbyDuplicate(nums []int, k int) bool {
dict := make(map[int]int)
for i:=0; i<len(nums); i++ {
v, ok := dict[nums[i]]
if ok {
if i - v <= k {
return true
}
}
dict[nums[i]] = i
}
return false
}

3、买卖股票的最佳时机:一个数组prices,prices[i]代表第i天的股票价格。设计一个算法能获取最大利润。

只能买卖一次。选择某一天买入这只股票,并选择在未来的某一天卖出该股票。计算能获取的最大利润。

func maxProfit(prices []int) int {
minPrice := prices[0]
profit := 0
for i:=1; i<len(prices); i++ {
if prices[i] < minPrice {
minPrice = prices[i]
} else if profit < prices[i] - minPrice {
profit = prices[i]-minPrice
}
}
return profit
}

可以尽可能的完成更多的交易,多次买卖股票,必须在再次购买前出售掉之前的股票。 使用贪心算法。计算的过程并非买卖的过程。初始化利润profit:=0。遍历prices,初始化I:=1,如果prices[I]>prices[i-1],那么就profit = profit + prices[I]-prices[I-1]。最终返回利润 profit.

func maxProfit(prices []int) int {
//贪心算法 profit := 0
for i:=1; i<len(prices); i++ {
if prices[i] > prices[i-1] {
profit = profit + prices[i] - prices[i-1]
}
}
return profit }

4、旋转数组:一个数组,将数组元素右移k个位置,其中k是非负数。空间复杂度为O(1)的原地算法。 解法一:遍历。复杂度 O(kn)。右移一位,就是把尾部元素插入到0的位置,也就是暂存last元素,然后从尾部遍历挪动其他元素,最后赋值暂存元素到首元素 nums[0]=last。 解法二:利用翻转。先对整个数组进行翻转,然后再对0到k元素,k到last元素分别进行翻转。

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

数组元素翻转:遍历数组nums,结束条件 len(nums)>>1 。 //翻转

func reverse(nums []int) {
l := len(nums);
for i:=0; i<(l>>1); i++ {
nums[i],nums[l-i-1] = nums[l-i-1], nums[i]
} }

5、只出现一次的数字。一个非空数组nums,除了某个元素只出现过一次,其他每个都出现两次。找出只出现一次的元素。具有线性时间复杂度O(n)。不借助外部存储空间。 异或^运算:0^b=b;0^0=0; 1^1=0; a^a=0;
满足交换律和结合律:a^b^a=(a^a)^b=b。 声明value=0,遍历数组,数组元素^value,赋值给value。最终的值就是要查找的元素。 相同元素异或等于0;一个非0元素异或上0等于它自己;异或遵循交换律和结合律;

func singleNumber(nums []int) int {
n := 0
for _, v := range nums {
n ^= v
}
return n
}

6、加一。一个整数数组nums,表示一个非负整数,在该数的基础上加1。

7、移动零。整数数组nums,将所有的0移动到末尾,同时保持非0元素的稳定性,即相对顺序。 双指针,left,right,循环条件right<length,nums[right] != 0 交换left、right并 left++。外层right++。

8、两数之和。整数数组nums,整数目标值target,在数组nums中找出和为目标值的两个整数,并返回它们的下标。 利用字典。遍历数组,获取元素值v,计算出x=target-v,

9、两个数组的交集。给定两个数组,计算它们的交集。nums1=[12341], nums2=[2116], 交集就是 [112]。可以有多个重复元素。 利用字典。先遍历len较小的数组nums1,把元素存储到字典中,key是元素值,value是出现的次数,dict[v]++。然后遍历nums2,如果元素在dict中存在,且值取出来是大于0的,那么就存储到结果数组result中,然后对dict[v]—。

func intersect(nums1 []int, nums2 []int) []int {
if len(nums1) > len(nums2) {
return intersect(nums2, nums1)
}
dict := make(map[int]int)
for _, v := range nums1 {
dict[v]++
}
result := make([]int, 0)
for _, v := range nums2 {
i, ok := dict[v]
if ok && i>0 {
result = append(result, v)
dict[v]--
}
}
return result
}

两个数组的交集。输出结果每个元素唯一。关键map删除元素:delete(dict, v)。 func intersection(nums1 []int, nums2 []int) []int { if len(nums1) > len(nums2) { return intersection(nums2, nums1) } dict := make(map[int]int) for i,v := range nums1 { dict[v] = i } result := make([]int,0) for _,v := range nums2 { _, ok := dict[v] if ok { result = append(result, v) delete(dict, v) } } return result }

10、山脉数组的峰顶索引。找出峰值元素:一个数组,元素先升序,再降序,超出其中的峰值元素。 利用二分搜索,迭代遍历,初始化left=0,right=len-1,然后遍历,结束条件left<right。mid = (left + right) >> 1。判断如果是nums[mid] > nums[mid+1] 那么就right= mid。否则 left = mid + 1,最终返回left。

func peakIndexInMountainArray(arr []int) int { // 时间复杂度:O(logn) left,right := 0, len(arr)-1 for left < right { mid := (left+right) >> 1 if arr[mid] > arr[mid+1] { right = mid } else { left = mid + 1 } } return left }

func peakIndexInMountainArray(arr []int) int { // 时间复杂度:O(n) maxIndex := 0 for i:=0; i<len(arr); i++ { if arr[i] > arr[maxIndex] { maxIndex = i } }
return maxIndex }

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

二分搜索BinarySearch:有序整型数组nums和一个目标值target,写一个函数搜索nums中target,如果存在返回下标,否则返回-1。 12345 func search(nums []int, target int) int { // [left right) 区间的元素总个数 right-left     left, right := 0, len(nums)     for left < right {         mid := (right + left) >> 1         if nums[mid] == target { //问题:如果有多个重复的值,返回的下标是不固定的             return mid         } else if target < nums[mid] {             right = mid         } else {             left = mid+1         }     }     return -1 }

11、找出数组中比左边大比右边小的元素。该元素比在它左边的元素都大,比它右边的所有元素都小。 时间复杂度O(n)。 先从右往左遍历,求所有元素的右边最小数,存放到一个数组rightMin中。然后从左往右遍历判断当前元素是否大于左边元素,如果是则和右边最小数rightMin中元素比较,如果小于则满足条件存储到结果数组中。

func find(nums []int) []int { l := len(nums)

var rightMins = make([]int, l)
rightMin := nums[l-1]
for i:=l-1; i>=0; i-- {
    if nums[i] < rightMin {
        rightMin = nums[i]
    }
    rightMins[i] = rightMin
}
var result = make([]int, 0)
leftMax = nums[0]-1
for i:=0; i<l; i++ {
    if leftMax < nums[i] && nums[i] < rightMins[i] {
        leftMax = nums[i]
        result := append(result, nums[i])
    }
}
return result

}

12、多数元素。长度为n的数组nums,找到其中多数元素。多数元素指在数组中出现次数大于[n/2]的元素。 多数元素大于n/2个,也就是比其他所有元素加起来还多。假设temp为众数,count为它出现的次数,当遇到和它不同的元素就count-1,相同元素就count+1,当count==0的时候,就转换count为当前元素,最终会temp会转换为多数元素。

// 方法一: func majorityElement(nums []int) int { // 时间复杂度O(n) 空间复杂度O(1) if len(nums) == 1 { return nums[0] } // 众数 temp := nums[0] count := 1 for i:=1; i<len(nums); i++ { if count == 0 { temp = nums[i] } if nums[i] == temp { count++ } else { count-- } } return temp }

// 方法二:先排序,再记录比较count `func majorityElement1(nums []int) int {

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

//排序
sort.Ints(nums)
//遍历
temp := nums[0]
count := 1
for i:=1; i<len(nums); i++ {
    if nums[i] == temp {
        count++
        if count > (len(nums)>>1) {
            return temp
        }
    } else {
        temp = nums[i]
        count = 1
    }
}
return -1

}

13、乘积最大子数组。整数数组nums,找出数组中乘积最大的连续子数组。子数组最少包含一个数字。返回该子数组对应的乘积。

输入:[2,3,-2,4] 输出:6,子数组 [2,3] 输入:[-2,0,-1] 输出:0

func maxProduct(nums []int) int { imax, imin, max := nums[0],nums[0],nums[0] for i:=1; i<len(nums); i++ { if nums[i] < 0 { imax,imin = imin,imax } imax = maxFunc(imax * nums[i], nums[i]) //-2 * 3 -2 = -2 8 imin = minFunc(imin * nums[i], nums[i]) // -2 * 3 -2 = -6 -4 max = maxFunc(imax, max) // -2 } return max }

func maxFunc(x, y int) int { if x > y { return x } return y }

func minFunc(x, y int) int { if x > y { return y } return x }

字符串相关 : 1、一个字符串/a/b/../c/./d,是目录层级 .. 代表回到上一级,. 当前目录,b代表a的子目录。简化目录。先切割成数组,数组元素a、b、..、c、. 、d,然后遍历,查看元素如果是字母,直接入栈,如果是..,舍弃当前元素,并出栈,如果是. ,就舍弃当前元素。最终栈中的元素就是简化后的。

2、回文数。验证整数x是否是回文整数。

func isPalindrome(x int) bool { y := x p := 0 for y > 0 { tail := y%10 y = y/10 p = p*10 + tail } if p == x { return true } return false }

3、最长回文串。给一个字符串,包含大消息字母,找到通过这些字母构成的最长的回文串。区分大小写,返回长度。

无重复字符的最长子串: 涉及到次数,需要用到散列表。构造子串,散列表存储下标。涉及子串,考虑滑动窗口。

单向链表是链式存储数据的,使用多少就申请多少内存空间,不会造成内存空间的浪费。LinkedList成员size、first,first是指向头节点。每个节点都包含一个next指针,指向下一个节点(Node)。链表根据index查询节点复杂度 O(n),添加节点那一刻复杂度是 O(1),添加之前需要查找,整体复杂度是 O(n)。 虚拟头结点:在头结点head前面增加的结点,不存储数据。让代码更精简,统一所有节点的处理逻辑。

单向链表:插入、删除某个节点,需要找到这个节点的上一个节点;边界为空的情况:head头节点、尾结点、尾结点的前一个节点;遍历链表for(node!=nil) {node = node.next};删除单向链表中给定的某个节点:让下一个节点存储的值依次覆盖上一个节点存储的值,且去掉最后一个重复的节点。

单向链表使用场景:哈希表。

(可以引出二叉树,只有右子树的二叉树就是单向链表……)

1、翻转链表,使用迭代法:首先创建newHead=nil,遍历,创建temp并移动到head的next,接着head的next指向newHead,然后newHead移动到head,最后head移动到temp。 /**

  • Definition for singly-linked list.
  • type ListNode struct {
  • Val int
    
  • Next *ListNode
    
  • } */ func reverseList(head *ListNode) *ListNode { var newHead *ListNode var temp *ListNode for head != nil { temp = head.Next head.Next = newHead newHead = head head = temp } return newHead }

2、环形链表,判断链表是否有环:创建慢指针slow、快指针fast,慢指针slow走一步slow=slow.Next,快指针fast走两步fast=fast.Next.Next,当慢指针和快指针相遇slow==fast就说明链表有环。

func hasCycle(head *ListNode) bool { var slow *ListNode fast := head for fast != nil && fast.Next != nil { if slow == fast { return true } fast = fast.Next.Next

    if slow == nil {
        slow = head
    } else {
        slow = slow.Next
    }
}

return false

}

3、移除链表元素,移除值为val的节点:创建newHead,last,遍历,如果newHead为空且head值不为val,初始化newHead、last为head,继续遍历head=head.Next,判断如果head的值不为val,设置last.Next为head,移动last到last的next,继续循环以上操作,最后处理尾结点。

func removeElements(head *ListNode, val int) *ListNode { newHead := &ListNode{} temp := newHead for head != nil { if head.Val != val { temp.Next = head temp = temp.Next } head = head.Next } temp.Next = nil

return newHead.Next

}

删除中间节点:链表中的某个节点既不是头结点,也不是尾结点,就叫中间节点。 给一个中间节点,将此节点从链表中删除。 /**

  • Definition for singly-linked list.

  • type ListNode struct {

  • Val int
    
  • Next *ListNode
    
  • } */ func deleteNode(node *ListNode) { for node.Next != nil { node.Val = node.Next.Val

     if node.Next.Next == nil {
         node.Next = nil
         break
     }
     
     node = node.Next
    

    } }

func deleteNode(node *ListNode) { *node = *node.Next }

4、删除排序链表中的重复元素,使得每个元素只出现一次:同删除链表元素差异判断条件,判断head的值不等于last的值,设置last.Next为head,移动last到last的next。

func deleteDuplicates(head *ListNode) *ListNode { newHead := &ListNode{} last := newHead for head != nil { if last == newHead { last.Next = head last = last.Next } else { if last.Val != head.Val { last.Next = head last = last.Next } } head = head.Next } last.Next = nil return newHead.Next }

5、删除排序链表中的重复元素,只保留没有重复出现的数字:创建虚拟头结点,循环遍历,先判断last的next的值不等于head的值,然后如果last的next的next节点为head(last->2->3head),则last移动到last的next,如果last的next的next节点不为head(last->2->2->3head),设置last的next指针为head。最后移动head到head的next。也就是last 指针是走一步,还是last的next指针走到head。

创建虚拟头结点 newHead := &ListNode{}; newHead.Next = head; last=newHead; for head != nil { if last.Next.Val != head.Next.Val { if last.Next.Next == head { last = last.Next } else { last.Next = head }} head = head.Next}

func deleteDuplicates(head *ListNode) *ListNode { newHead := &ListNode{} newHead.Next = head last := newHead for head != nil { // last>1>1>2 last>1>2 if last.Next.Val != head.Val { //如果last 和 head值相等,head指针走一步 //last -> 2 ->2 -> 3 head //last -> 2 -> 3 head if last.Next.Next == head { //head是 last.Next.Next 节点,last > 2 > 3 head last = last.Next // last指针走一步 } else { // last直接走到head, last > 2 > 2 > 3 head last.Next = head } } head = head.Next }

//清除尾部元素
if last.Next != nil && last.Next.Next != nil {
    last.Next = nil
}

return newHead.Next

}

6、链表的中间节点,如果是两个返回第二个节点:使用map或者数组。map把链表index为key,节点为value,通过中间的index,取出中间节点;数组把链表元素一次存入数组,通过数组下标取出中间节点。

不借助其他存储空间:快慢指针。 slow = slow.Next; fast = fast.Next.Next; fast走完,slow刚好走一半。

func middleNode(head *ListNode) *ListNode { left := &ListNode{} left.Next = head right := head // 1 2 3 // 1 2 3 4 for right != nil && right.Next != nil { right = right.Next.Next left = left.Next } return left.Next }

7、链表的倒数第几个节点,使用快慢指针法,或者链表转为数组或map来直接获取。

删除链表倒数第N个节点:

func removeNthFromEnd(head *ListNode, n int) *ListNode { dummy := &ListNode{} dummy.Next = head slow := dummy fast := head for i:=0; i<n; i++ { fast = fast.Next }

for ;fast != nil; fast = fast.Next {
    slow = slow.Next
}

//移除 slow.Next
slow.Next = slow.Next.Next

return dummy.Next

}

8、回文链表。判断一个链表是否是回文链表。 首先找到链表的中间节点 firstHalfEnd ,然后翻转后半部分链表。遍历两个链表比对元素,如果不相等就返回false,判断是否回文

func isPalindrome(head *ListNode) bool { //先拿到中间节点,及长度 //在翻转后面节点 1221 -> 1212 //遍历比对 summy := &ListNode{} summy.Next = head slow := summy fast := head // 1 > 2 > 1 // 1 > 2 > 2 > 1 for fast != nil && fast.Next != nil { fast = fast.Next.Next slow = slow.Next if fast != nil && fast.Next == nil { slow = slow.Next } }

// 翻转
var newHead *ListNode
var temp *ListNode
halfNode := slow.Next //head
for halfNode != nil {
    temp = halfNode.Next
    halfNode.Next = newHead
    newHead = halfNode
    halfNode = temp
}

for newHead != nil {
    summy = summy.Next
    if summy.Val != newHead.Val {
        return false
    }
    newHead = newHead.Next
}

return true

}

双向链表:比单向链表多了一个pre指针,head的pre指向nil,提升了链表的综合性能。适合的场景:频繁在头部、尾部、任意位置进行添加和删除元素的场景;如果是频繁查询,随机访问使用数组。

单向循环链表:尾结点的next指向head。 双向循环链表:head的pre指向尾结点,尾结点的next指向head。可以增加:current;reset让current移动到头结点;next 让current往后走一步current=current.Next。约瑟夫环问题。 静态链表:使用数组来模拟,数组元素struct,next是下标。

栈:只能在一端进行操作,入栈push向栈中添加元素,出栈pop移除栈顶元素,获取栈顶元素Top。

1、使用切片或数组来模拟栈,切片首元素为栈底,末尾元素为栈顶。 切片的 last 元素就是栈顶元素。push操作就是在尾结点append追加元素。

2、浏览器的前进和后退,后退就是从第一个栈中pop掉栈顶元素,并把元素push到第二个栈中,前进把第二个栈的栈顶元素pop掉,并push到第一个栈中;

3、涂鸦的撤销、恢复功能。

4、有效括号:只包含’{’,’}’,’(’,’)’,’[’,’]’ 的字符串,判断有效性,左括号用相同的右括号闭合,闭合顺序正确。创建map[byte]byte{},左括号为key右括号为value,创建栈make([]byte,0)用切片模拟栈,遍历字符串,判断字符是否在map的key中,如果在map的key中就入栈push,不在就判断该字符和栈顶元素从map中取出的value是否相等,不等就返回fase。最后判断stack元素为空返回true。

5、用栈来实现队列:入队push到inStack中,出队判断outStack为空就将inStack所有元素逐一pop并push到outStack,outStack弹出栈顶元素。如果outStack不为空,outStack弹出栈顶元素。

6、使用堆来模拟栈

队列:入队enQueue从队尾rear添加元素,出队deQueue从队首front移除元素,获取队的头元素front。队列是从首尾操作元素,优先使用双向链表来实现。peekFirst查看队列的头元素。peekLast查看队列的尾元素。

(用两个栈来实现队列…)

用动态数组实现队列:enQueue直接向数组最后面追加元素elements=append(elements, e),deQueue取切片elements=elements[1:],front始终是0,判断size不等于0。

循环队列CircleQueue:底层使用数组实现。正常情况下,队首元素front是数组前面的index,队尾是数组尾部方向的元素。队首元素front数组的index,队列长度size。队尾入队enQueue计算队尾插入位置index是(front+size)%capacity,0123->(3+2)%4=1;头部出队deQueue计算新的队首元素index是 front = (front + 1)%capacity,0123->(3+1)%4=0;遍历获取元素在数组中的真实的索引:(front + i) % capacity;扩容需要把front放在新数组0的位置; 计算真实索引index:之前的索引对容量capacity取模就能获取到真实的索引,抽出方法 (front+index)%capacity。

双端循环队列:两端都可以进行删除、添加操作。增加了从头部入队enQueueFront,(-1 + front)如果是负数,索引为 (-1 + front)+capacity,为正数索引是(-1 + front)%capacity;从尾部出队deQueueRear,索引算法和头部入队相同(size-1 + front)判断负数。头部front,尾部是rear=(front+size-1) % capacity。

乘法、除法、取模、浮点数运算效率较低。

优化取模运算:a%b,如果a小于b,则a对b取模就是a;如果a大于等于b,且a不大于b的二倍,a对b取模就是a-b。提取代码:a-(a>=b?b:0)。也就是:index-(capacity > index ? 0 : capacity) ,如果为负数,就 index+capacity。

与动态数组的优化类似。

用队列来实现栈:

优先级队列,Priority Queue:使用二叉堆BinaryHeap实现。 使用:线程调度,。

(二叉堆….)

树 -> 二叉树(度最大为2)-> 真二叉树(度为0或2)-> 满二叉树(度为0或2且叶子节点在最后一层)-> 完全二叉树(从上到下、从左到右)。

树Tree:一棵树最多只有一个根节点,也可以只有根节点,也可以是空树没有节点的。 节点的度degree就是:节点子树的个数,度为0的节点叫叶子节点leaf。 层数level:根节点在第1层,根节点的子节点在第2层,以此类推。 节点的深度depth:从根节点到当前节点的唯一路径上的节点总数。节点深度的最大值就是树的深度。 节点的高度height:从当前节点到最远叶子节点的路径上的节点总数。节点高度的最大值就是树的高度。树的深度等于树的高度。 森林:由m棵互不相交的树组成的集合,m大于等于0。

树Tree可以大大提高效率。

二叉树BinaryTree:每个节点的度(子树数量)最大为2,左子树和右子树是有顺序的。特殊二叉树:空树、一个节点树、只有右子树就是单向链表。

真二叉树Proper BinaryTree:没有度为1的节点。(度为0或2)

满二叉树Full BinaryTree:没有度为1的节点,且所有叶子节点都在最后一层。满二叉树一定是一棵完全二叉树。

完全二叉树:从上到下,从左到右,节点排满。叶子节点只能出现最后两层;最后1层的叶子节点都靠左对齐;度为1 的节点只有左子树;度为1的节点最多只有1个;完全二叉树从根节点到倒数第2层是一棵满二叉树;同样节点数量的二叉树,完全二叉树的高度最小。

二叉搜索树效率高:添加、删除、搜索的最坏时间复杂度均可优化至 O(logn)。 有序动态数组二分搜索最坏时间复杂度为:O(logn);添加、删除、遍历查找的平均时间复杂度是O(n);

二叉搜索树Binary Search Tree,BST:任意一个节点的值,都大于这个节点左子树的所有节点值,都小于这个节点右子树的所有节点值;二叉搜索树的左右子树也是一棵二叉搜索树;二叉搜索树存储的元素必须具备可比较性,指定比较方式;不能为nil;没有索引,元素和添加顺序无关。

接口设计:size、isEmpty、clear、add(e)、remove(e)、搜索contains(e)。节点Node包含:left、right、parent。

添加节点:先找到父节点parent,然后创建新节点node,比较值大小,如果新节点比父节点小就赋值为左子树parent.left = node,如果新节点比父节点大就赋值为右子树parent.right = node;如果相等,就直接覆盖;初始化的时候传入比较器block或闭包,或者传入对象支持协议方法,也可以两者都支持。

性质1,求叶子节点数:所有二叉树叶子节点个数n0等于度为2的节点个数n2加1,即n0=n2+1。 推导: 节点总数n = n0+n1+n2; 总边数=n-1; 度为0的节点边数为0,度为1的节点边数为1,度为2的节点边数为2,总边数=n1+2*n2。

(叶子节点个数n0,度为1的节点个数n1,度为2的节点个数n2)

性质2,完全二叉树,求总结点数n的完全二叉树的叶子节点数: 如果一棵完全二叉树有768个节点,求叶子节点的个数: 推导: 总结点数为n,叶子节点数为n0,度为1的节点数n1,度为2的节点个数n2。 n = n0 + n1 + n2; n0 = n2 + 1; => n = 2 * n0 + n1 - 1; 因 n1 = 0 或 1; 当 n1 = 1 时,那么n = 2 * n0,n 是偶数,n0=n/2,非叶子节点数 == n - n0 == n/2; 当n1 = 0时,那么n = 2 * n0 - 1,n 是奇数,叶子节点个数n0等于(n + 1) / 2,非叶子节点个数 = n - n0 == (n-1)/2。 叶子节点的个数为 (n+1)/2 向下取整,也是(n+1) >> 1 ;非叶子节点个数为 n/2 向下取整。

性质3,满二叉树第i层节点数量是:2^(i-1) 。

性质4,高度为h的满二叉树叶子节点数量是:2^(h-1),总结点数量是:2^h - 1 = 2^0+2^1+….+2^(h-1)。在同样高度的二叉树中,满二叉树的叶子节点数量最多,总结点数量最多。

(公式:2^0+2^1+…….+2^n = 2^(n+1)-1)

性质5,完全二叉树,求总结点数n的完全二叉树的高度:高度为h的完全二叉树(h>=1),那么至少有2^(h-1)个节点=2^0+2^1+…+2^(h-2)+1。最多就是满二叉树2^h - 1个节点。总结点数就在2^(h-1) 和 2^h 之间,2^(h-1) <= n < 2^h,分别取对数 h-1<log2^n<h,然后log2^n < h < log2^n + 1,h就等于log2^n向下取整加1。

性质6,求完全二叉树第i个节点的父节点索引和子节点索引: 有n个节点,n>0,从上到下、从左到右对节点从1开始进行编号,对任意第i个节点: 如果i=1,它是根节点; 如果i>1,它的父节点编号为floor(i/2)向下取整; 如果2i <= n,它的左子节点编号为2i,也就是2i如果不大于总结点数,2i就是节点i的左子节点数,如果大于总结点数就是没有左子节点; 同样右子节点数为2*i+1。

(二叉堆……..) (二叉堆使用这个性质,二叉堆是一个完全二叉树,使用数组实现,需要求某个二节点的子节点,父节点)

二叉树翻转:入参root节点,判断root节点为nil返回,不为nil,交换root节点的left、right节点,然后递归翻转root节点的左右子节点root.Left,root.Right。

线性数据结构的遍历分为正序遍历、逆序遍历。两个方向。

二叉树的遍历:(不是二叉搜索树特有的)

前序遍历PreorderTraversal:依次遍历根节点、左子树、右子树。 给你一个二叉树的根节点root,返回它节点值的前序遍历,递归实现:先新建切片result,判断根节点不为空,数据存储到切片中,然后递归调用,传入根节点的左子树,返回数据切片,不为空追加切片元素到切片result中,接着继续递归调用,传入根节点的右子树,返回数据切片,不为空追加切片元素到切片result中,最终返回切片result。

中序遍历InorderTraversal:依次遍历左子树、根节点、右子树。二叉搜索树中序遍历的结果是升序;后序遍历PostorderTraversal:依次遍历左子树、右子树、根节点; 根节点前面遍历就是前序,中间遍历就是中序,后面遍历就是后序。

层序遍历LevelOrderTraversal访问顺序:从上到下、从左到右依次访问每一个节点。(重要)

思路:使用队列。首先将根节点入队,然后将队首元素A出队访问,并将A的左、右子节点依次入队,继续循环,直到队列为空为止。

实现过程:首先创建队列,并把root根节点入队deQueue,queue := []TreeNode{root},结果二维数组result = make([][]int, 0) ,创建每层结果数组level=make(int[],0),创建当前层节点数量size=1只有根节点,下一层节点数存储nextSize := 0,然后循环判断队列queue是否为空,如果为空,就结束循环。如果不为空就循环,队首元素出队deQueue,判断左右节点是否为nil,不为nil把左节点、右节点依次存入队列中,把出队的元素的Val存入到level数组,level = append(level, root.Val),每次循环size- - ,如果有左右节点,就nextSize++,如果size==0,就代表本层结束,把level存入到result二维切片中,清空level,size =nextSize,清空nextSize。下一层的元素个数,就是当本层元素遍历完成,队列的size。可以计算二叉树的高度,二维数组包含一维数组的个数就是层高。

遍历应用1,利用层序遍历,判断一棵树是否为完全二叉树:如果树不为nil,开始层序遍历二叉树(用队列)。如果node.left != nil,将node.left 入队;如果node.left==nil && node.right != nil,返回false;如果node.right != nil,将node.right入队;如果node.right==nil,那么后面遍历的节点都为叶子节点才是完全二叉树,否则返回false。 四种情况组合:左右子节点为空不为空。

遍历应用2,利用层序遍历计算二叉树的高度。

遍历应用3,利用前序遍历打印二叉树:入参root,locationStr,prefix,首先拼接占位文案locationStr = locationStr + prefix,打印占位文案locationStr、root.Val,然后递归打印左子节点、右子节点。

遍历应用4,利用中序遍历对二叉搜索树元素升序排列。

遍历应用5,利用后序遍历对一些先子后父的操作。

前驱节点predecessor:中序遍历时的前一个节点。 后继节点successor:与前驱相反,中序遍历时的后一个节点。 二叉搜索树的删除用到前驱和后继。

对于二叉搜索树:前驱节点就是前一个比它小的节点,也就是左子树的最大节点; 如果 node.left != nil,那么前驱节点prodecessor = node.left.right.right…… ,终止条件:right == nil; 如果 node.left == nil && node.parent != nil,prodecessor=node.parent.parent.parent…. 遍历node=node.parent,终止条件node在parent的右子树中node==node.parent.right,如果没有右子树,就没有前驱节点; 如果node.left == nil && node.parent == nil,那么就没有前驱节点;

二叉搜索树删除节点:删除叶子节点,判断是父节点的左右子节点,通过父节点删除node.parent.left=nil;删除度为1的节点,用子节点替代原节点位置child.parent=node.parent,node是左子节点就把node.parent.left=child,如果是右子节点就把node.parent.right=child,如果是根节点root=child,child.parent=nil;删除度为2的节点,;

二叉搜索树的复杂度是:O(h) == O(logn)。二叉搜索树退化成链表后复杂度:O(h)==O(n)。当n比较大的时候,这两种情况性能差异比较大。比如n=一百万次约为2^20,二叉搜索树最小搜索20次,最多一百万次。 添加、删除节点的时候,二叉搜索树都可能会退化成链表。 防止二叉搜索树退化成链表:让添加、删除、搜索的复杂度维持在O(logn)。 平衡 Balance:当节点的数量固定的时候,左右子树的高度越接近,这棵二叉树越平衡(高度越低)。 改进二叉搜索树:节点的添加、删除顺序是无法限制的,可以认为是随机的。所以改进方案:在节点添加、删除操作之后,用尽量少的调整次数达到适度平衡即可,想办法让二叉搜索树恢复平衡(减小树的高度)。

平衡二叉搜索树 Balanced Binary Search Tree,BBST:一棵达到适度平衡的二叉搜树。常见的平衡二叉搜索树:AVL树,Windows NT内核中广泛使用。红黑树:C++ STL(比如map、set);JAVA的TreeMap、TreeSet、HashMap、HashSet;Linux的进程调度;Nginx的timer管理; AVL树、红黑树也叫自平衡二叉搜索树,Self-balancing Binary Search Tree。

AVL树: 平衡因子,Balance Factor:某节点的左右子树的高度差。叶子节点左右子树的高度差是0,所以叶子节点的平衡因子是0。 AVL树的特点:每个节点的平衡因子只可能是1、0、-1(绝对值<=1,如果超过1,称之为失衡);每个节点的左右子树高度差不超过1;搜索、添加、删除的时间复杂度是O(logn);

AVLTree、RBTree继承自BST,BST继承自 Binary Tree。

AVL树添加元素:可能会导致所有祖先节点都失衡;只要让高度最低的失衡点恢复平衡,整棵树就恢复平衡,仅需O(1)次调整。 AVL树删除元素:只可能会导致父节点失衡;让父节点恢复平衡后,可能会导致更高层次的祖先节点失衡,最多需要O(logn)次调整。 平均时间复杂度:搜索O(logn);添加O(logn),仅需要O(1)次的旋转操作;删除O(logn),最多需要O(logn)次的旋转操作;

LL - 右旋转(单旋),RR-左旋转(单旋),LR - RR左旋转,LL右旋转(双旋),RL - LL右旋转,RR左旋转(双旋)。

红黑树 Red Black Tree:一种自平衡的二叉搜索树。也叫平衡二叉B树 Symmetric Binary B-tree。红黑树必须满足以下5条性质:1、节点是Red或者Black。2、根节点是Black。3、叶子节点(外部节点,空节点)都是Black。4、Red节点的子节点都是Black。Red节点的parent都是Black。从根节点到叶子节点的所有路径上不能有2个连续的Red节点。5、从任一节点到叶子节点的所有路径都包含相同数目的Black节点。

B树:一种平衡的多路搜索树,多用于文件系统、数据库的实现。 B树特点:1个节点可以存储超过2个元素、可以拥有超过2个子节点。拥有二叉搜索树的特点。平衡,每个节点的所有子树高度一致。比较矮。 m阶B树就是每个节点最多有m个子树。

二叉堆,BinaryHeap:提供三个接口,添加元素,获取最大值,删除最大值。 使用动态数组或双向链表,获取、删除最大值复杂度是O(n),添加元素复杂度是O(1)。 有序动态数组或双向链表,需要全排序,从小到大,获取最大值、删除最大值复杂度是O(1),添加元素复杂度是O(n)。 BBST红黑树,获取最大值,删除最大值,添加元素复杂度都是O(logn)。 堆,获取最大值O(1),删除最大值、添加元素都是O(logn)。

Top K问题:从海量数据中找出前K个数据。比如,从100万个整数中查找最大的100个整数。解法一:可以用堆来解决。

堆(Heap),一种树状的数据结构。不要跟内存模型中的堆空间混淆。常见的堆实现有:二叉堆Binary Heap也叫完全二叉堆,等其他。 堆的特点:任意节点的值总是大于等于>=或小于等于<=子节点的值。 如果任意节点的值总是大于等于>=子节点的值,称为大顶堆。 如果任意节点的值总是小于等于<=子节点的值,称为小顶堆。 必须具有可比较性,和二叉搜索树一样。

二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆,比完全二叉堆多了一个比较,大顶堆是根节点值大于子节点,小顶堆是根节点的值小于子节点。 二叉堆底层一般使用数组实现。

二叉堆实现接口:size、comparator、isEmpty、clear、add、get、remove、replace

大顶堆为例。 添加元素实现:先把元素追加到数组中,然后判断新元素和父节点大小,比父节点大就和父节点交换位置,循环。如果是比父节点小或没有父节点结束循环。这个比较的过程叫做上滤siftUp。

比较排序Comparison Sorting:冒泡、选择、插入、归并、快速、希尔、堆排序。

冒泡排序Bubble Sort:从头开始比较相邻的两个元素,如果第一个数大于第二个数进行交换。优化1:全部有序;优化2:局部有序;平均、最坏时间复杂度O(n^2)。最好时间复杂度:O(n)。

/// 冒泡排序 func bubbleSort(s []int) { for end := len(s)-1; end > 0; end-- { for begin := 1; begin <= end; begin++ { if s[begin] < s[begin-1] { //稳定性,等于不要交换 //交换 temp := s[begin] s[begin] = s[begin-1] s[begin-1] = temp } } } fmt.Println("s:", s) }

排序算法的稳定性Stability:如果相等的2个元素,在排序前后的相对位置保持不变,它就是稳定的。

原地算法In-place Algorithm:不依赖额外的资源或者少数的额外资源,仅依靠输出来覆盖输入。空间复杂度比较低,空间复杂度为O(1)。

选择排序Selection Sort:从序列中找出最大元素的下标maxIndex,然后与最末尾的元素交换位置。最好、最坏、平均时间复杂度O(n^2)。获取最值可以使用堆来处理,时间复杂度可以达到O(logn)。

/// 选择排序 func selectionSort(s []int) { for end := len(s)-1; end > 0; end-- { maxIndex := 0 for begin := 1; begin <= end; begin++ { if s[maxIndex] <= s[begin] { //保持稳定性 maxIndex = begin } } temp := s[maxIndex] s[maxIndex] = s[end] s[end] = temp }

fmt.Println("s:", s)

}

堆排序Heap Sort:对选择排序的一种优化。1、对数组进行原地建堆,shiftDown、shiftUp。2、交换。

插入排序 Insertion Sort:扑克牌排序。把数据分为两部分:有序的头部和待排序的尾部。从头开始扫描每一个元素,每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据保持有序。 插入排序的复杂度与逆序对 Inversion 成正比关系,逆序对越多,复杂度越大。逆序对就是比较需要交换的两个数就是一对逆序对。 平均、最坏复杂度是O(n^2)。最好复杂度是O(n)。 当逆序对数量极少的时候,插入排序的效率特别高,甚至速度比O(nlogn)级别的快速排序还要快。 数据量不是很大的时候,插入排序效率也是很高的。

// 插入排序 func InsertionSort(nums []int) []int { for begin := 1; begin < len(nums); begin++ { cur := begin for cur > 0 && nums[cur] < nums[cur-1] { //稳定排序 temp := nums[cur] nums[cur] = nums[cur-1] nums[cur-1] = temp cur-- } } return nums }

优化1:将 交换 修改为 挪动。首先,对待插入的元素备份,然后遍历有序数据,判断元素比插入元素大,元素就朝尾部挪动1个位置,直到元素比插入元素小,停止遍历,把待插入元素放到最终的位置。

func InsertionSort2(nums []int) []int { for begin := 1; begin < len(nums); begin++ { cur := begin temp := nums[cur] for cur > 0 && temp < nums[cur-1] { nums[cur] = nums[cur-1] cur-- } nums[cur] = temp } return nums }

二分搜索BinarySearch:有序整型数组nums和一个目标值target,写一个函数搜索nums中target,如果存在返回下标,否则返回-1。

func search(nums []int, target int) int { // [begin end) 区间的元素总个数 end-begin     begin, end := 0, len(nums)     for begin < end {         mid := (end - begin) >> 1         if nums[mid] == target { //问题:如果有多个重复的值,返回的下标是不固定的             return mid         } else if target < nums[mid] {             end = mid         } else {             begin = mid+1         }     }     return -1 }

优化2:使用二分搜索来优化插入排序。使用二分搜索来搜索插入位置。 二分搜索,当target < nums[mid]的时候,从左边搜索,其他的从右边搜索,最终返回要插入位置的下标。减少了比较次数,插入排序的平均时间复杂度没有变化O(n^2)。

// 插入排序 - 优化:二分查找 -> 挪动 -> 插入 func InsertionSort3(nums []int) []int { for begin := 1; begin < len(nums); begin++ { temp := nums[begin] // 二分查找 -> 查找到位置 insertIndex := searchIndex(nums, begin)

    // 元素挪动
    for i := begin; i > insertIndex; i-- {
        nums[i] = nums[i-1]
    }
    nums[insertIndex] = temp
}
return nums

} // 时间复杂度:O(logn) func searchIndex(nums []int, index int) int { begin, end := 0, index for begin < end { mid := (begin + end) >> 1 if nums[index] < nums[mid] { end = mid } else { begin = mid + 1 } } return begin }

归并排序Merge Sort:O(nlogn)。 快速排序 Quick Sort:最快。

网络:

计算机之间通信是依据对方的IP地址,网卡地址MAC地址,传送的数据最终被网卡接收。传输的数据包括:源IP地址、目标IP地址、源MAC地址、目标MAC地址。如果网卡发现目标MAC地址是自己,就会将数据传递给上一层进行处理。

首先发送一个ARP广播协议,获取对方的MAC地址,在同一个网段中传播。一个完整的ARP请求,分三次:首先计算机0发送广播,获取计算机1的mac地址,然后计算机0发送给计算机1,计算机1回复。ARP是有缓存的。

网线、同轴电缆、集线器有冲突域。什么是冲突域? 网桥Bridge:有两个接口,能够通过学习知道每个电脑的MAC地址在网桥的哪一侧,从而起到隔绝冲突域的作用。 交换机:相当于接口更多的网桥,全双工通信,是局域网解决方案。交换机在发送ARP广播的时候会记录局域网中所有计算机的MAC地址,就不会把数据传输给其他的计算机。比集线器安全,因为集线器不知道其他的计算机的MAC地址,会把数据发给所有人。全球网络的话,使用交换机,ARP广播就会全球发送,会引起广播风暴,也是比较浪费,就有了路由器。 路由器:可以在不同网段之间转发数据,隔绝广播域。同一个网段就是同一个广播域。

MAC地址 Media Access Control Address,有6字节(48bit)。前三个字节:OUI,组织唯一标识。后三个字节:网络接口标识。48位全为1代表广播MAC地址FFFF.FFFF.FFFF。

IP地址Internet Protocol Address:互联网上的每一个主机都有一个IP地址。IPV4 32bit,4字节,1个字节8位算是一部分,共四部分。IPV6 128bit,16字节。IP地址功能分为2部分:网络标识,即网络ID,主机标识,即主机ID。

网段就是:IP地址按位与&子网掩码。 IP地址:193.168.1.1 子网掩码:255.255.255.0 1100 0000.1010 1000.0000 0001.0000 1010 IP地址 & 1111 1111.1111 1111.1111 1111.0000 0000 子网掩码

1111    1111.1111   1111.1111     1111.0000   0000     网段
192.168.1.0     网段

IP地址:139.168.1.1 子网掩码:255.255.0.0 网段就是:139.168.0.0

A类地址:子网掩码255.0.0.0,第一部分对应网络ID,后面三部分对应主机ID。第一部分必须以0开头,所以网络ID范围:二进制7个0到7个1,十进制0到127。127作为保留网段。127.0.0.1 是本地回环地址Loopback,代表本机地址。网络ID第一部分是1到126就是A类地址。后面三部分的取值是 0 到255。主机部分,全0代表网段,全1代表广播地址,所以某个A类网段能容纳的主机数256256256 - 2 =2^24-2。

B类地址:子网掩码 255.255.0.0,前两部分是网络ID,第一部分必须是以10开头。每一部分有8位。 C类地址:默认子网掩码255.255.255.0,前三部分是网络ID,第一部分必须是以110开头。

123.210.100.200/16,代表子网掩码有16个1,也就是255.255.0.0,每一部分8位。

合理进行子网划分,解决的问题是:避免IP地址浪费。

私网IP访问Internet需要进行NAT转换为公网IP,NAT,Network Address Translation,由路由器来完成。节约网络IP资源,隐藏内部真实IP。

在实际的使用中,一般都是把网络划分为五层:应用层、传输层、网络层、数据链路层、物理层。

集线器工作在:物理层,没有智商,不会判断原mac地址、目标mac地址。 网卡是工作在:物理层、数据链路层。 交换机工作在:物理层、数据链路层,会判断原Mac地址、目标Mac地址。 路由器工作在:物理层、数据链路层、网络层。

物理层Physical:发送比特流Bits,定义接口标准,线缆标准,传输速率,传输方式等。

数据链路层Data Link:发送数据帧Frame,至少64字节,加上原Mac地址,目的Mac地址。首部+数据+尾部,数据部分是上一层传下来的。 链路:一个节点到相邻节点的一段物理线路,路由器到路由器或交换机,就是链路。

wireshark抓包是Ethernet II,以太网帧。 wireshark看不到数据链路层的尾部,只能看到首部。

网络层Network:发送数据包Packet,由首部、数据两部分组成。数据部分很多时候是由传输层传递下来的数据段Segment。首部有20个字节是固定的,一共五行,每行占用四个字节,每行是0到31位共32位。 以太网帧首部和网络层首部是挨在一起的。网络层首部可变部分最多是40个字节。 网络层数据太大,会分片传给数据链路层,转成多个数据链路层的帧,接收方在网络层会把数据组合起来。如何合并起来的,怎么区分是一个数据包?

网络层首部,标识Identification占16位,数据包的ID,当数据包过大进行分片时,同一个数据包的所有片的标识都是一样的。有一个计数器专门管理数据包的ID,每发出一个数据包,ID就加1。 如果数据包太大,超过了ID最大值,就会重头从1开始算起。有片偏移Fragment Offset来定位这个片在哪个位置,片偏移有13位,为了能够表示更多的偏移,所以这里存储的不是真实的字节偏移,片偏移乘以8才是字节偏移,Wireshark抓包出来的是已经乘过8的。

网络层首部,Flags标记是否有更多分片:More fragments: Set 1 有更多分片。

网络层首部,协议Protocol:占8位,表明所封装的数据是使用了什么协议。 ICMP : 1,IP:4,TCP:6,UDP:17,IPV6:41。

网络首部,校验和Header Checksum:用于检查首部是否有错误。

有些协议直接工作在网络层:ARP、IP、ICMP,就没有上面的应用层、传输层。 ICMP没有传输层的哪些可靠控制、流量控制、拥塞控制,所以是在网络层。

网络层首部,生存时间TTL:占8位,每个路由器在转发之前会将TTL减1,一旦发现TTL减为0,路由器会返回错误。观察ping命令后的TTL,能够推测出对方的操作系统,及中间经过了多少个路由器。 生存时间TTL是由操作系统默认的:Windows是 128,Linux 2.0.x kernel是64,Mac OS 是64。

ping /? 查看ping的用法 ping ip 地址 -I 数据包大小, sudo ping ke.qq.com -l 4000 ping ip地址 -f 不允许网络层分片

Wireshark抓包可以看到网络层:Internet Protocol Version 4,Src发送方IP,Dst接收方的IP。

传输层Transport:发送数据段Segments,使用TCP、UDP协议,保证可靠传输,发现某一段数据传输失败,重传。

TCP和UDP的区别,报文格式:

TCP,Transmission Control Protocol,传输控制协议:面向连接的;可靠传输,不丢包;首部占用空间大;传输速率慢;资源消耗大;应用场景:浏览器、文件传输、邮件发送; 对应的应用层协议:HTTP、HTTPS、FTP、SMTP、DNS。

UDP,User Datagram Protocol,用户数据报协议:无连接的;不可靠传输,尽最大努力交付,可能丢包;首部占用空间小;传输速率快;资源消耗小;应用场景:音视频通话、直播,注重实时的传输; 对应的应用层协议:DNS。

UDP是无连接的,减少了建立和释放连接的开销;尽最大能力交付,不保证可靠交付;不需要维护一些复杂的参数,首部只有8个字节,TCP至少20个字节。 UDP首部8个字节包括:16位源端口号、16位目的端口号、16位UDP长度、16位UDP检验和Checksum。

UDP首部: UDP端口号:占用2个字节,可推测出端口号取值范围:0~65535。 客户端的源端口是临时开启的随机端口。

UDP长度:16位2个字节:首部长度+数据的长度。

检验和Checksum:16位2个字节:伪首部+首部+数据。 伪首部12个字节包括:源IP地址,目的IP地址,UDP协议17,UDP长度。伪首部pseudo-header不会传递给网络层。

Wireshark抓包显示:User Datagram Protocol,src Port: ,Dst Port。

netstat -an 查看被占用的端口 netstat -anb 查看被占用的端口、占用端口的应用程序

TCP: 数据偏移:占4位,乘以4是首部的长度Header Length。因为有20个字节的固定首部,所以数据偏移至少是5,取值范围是0x0101到0x1111,最大是60个字节。和网络层首部一样。理解为:首部的长度就是数据向右边偏移了多少。

Reserved 保留位。

使用首部的一些字段保证可靠传输。

TCP/UDP的数据长度,完全可以由网络层IP数据包的首部推测出来: 传输层的数据长度 = 网络层的总长度 - 网络层的首部长度 - 传输层的首部长度。 网络层的数据部分就是传输层(传输层首部+传输层数据)。

TCP首部标志位Flags: URG,Urgent,当URG=1的时候,紧急指针字段才有效,表示当前报文段中有紧急数据,在TCP数据段前面的多少位是紧急数据,应优先尽快传送; ACK,Acknowledgment确认,当ACK=1的时候,确认号字段才有效; PSH,Push; RST,Reset,当RST=1的时候,表明连接中出现严重差错,必须释放连接,然后再重新建立连接; SYN,Synchronization,当SYN=1、ACK=0时,表明这是一个建立连接的请求,告诉服务器我想和你建立连接。如果对方同意建立连接,则回复SYN=1,ACK=1; FIN,Finish,当FIN=1的时候,表明数据已经发送完毕,要求释放连接。

Seq序号Sequence Number:占4个字节。在传输过程中的每个字节都有一个编号。连续的字节,编号是连续的。在建立连接后,序号代表:这一次传给对方的TCP数据部分的第一个字节的编号。建立连接的时候是0。

确认号,Acknowledgment Number:占4个字节,在建立连接后,确认号代表:期望对方下一次传过来的TCP数据部分的第一个字节的编号。

可靠传输: 停止等待ARQ协议,自动重传请求 Automatic Repeat-reQuest: A发送数据M1给B,B等待接收M1完成后,告诉A确认接收完成,然后A接收到确认,继续发送M2。 情况1:A发送M1给B的过程中出现超时,会重传M1,B就丢弃上次超时错误的报文。 情况2:A发送M1给B成功,B告诉A确认M1接收完成,确认M1的过程中超时,A会重传M1,B接收到重复的M1会丢弃,并重传确认M1。或者过了很长时间,B接收到了第一次迟到的确认M1,B什么都不做,丢掉。超时重传。

效率比较低。

改进:连续ARQ协议+滑动窗口协议。 连续ARQ指一次发送连续的几个数据。 A发送窗口中有4个分组M1、M2、M3、M4,发送完成后,停止发送,等待B的确认。B收到全部数据M1、M2、M3、M4,确认M4。M1、M2、M3、M4是连续的,所以只确认M4就行。A收到确认M4后,窗口滑动到M5、M6、M7、M8,发送窗口中的数据,发送完成后,停止发送,等待B的确认。

异常情况: 如果接收窗口最多能接收4个包,但发送方只发了2个包。 接收方如何确认后面还有2个包? 等待一定时间后没有第3个包,就会返回确认收到2个包给发送方。

滑动窗口:窗口大小是由接收端B端告诉你,由接收端决定。B端接收缓存有多大,就告诉A滑动窗口多大,可以发送多少个字节。接收端B端给A端发送确认消息的时候,会告诉A端B当时可以接收的数据缓存大小,也就是滑动窗口大小,滑动窗口可能每次的大小不一样。

窗口Window:占2字节,这个字段有流量控制功能,用以告知对方下一次允许发送的数据大小,单位是字节。

SACK 选择性确认 Selective Acknowledgment: 在TCP通信过程中,会出现发送序列中间某个数据包丢失的情况,比如M1、M2、M3、M4中丢失了M3。 如果没有SACK的情况,TCP会重传最后确认的分组后续的分组,最后确认的是M2,会重传M3、M4。这样就会导致已经正确传输的M4也重新发送,降低了TCP性能。 有了SACK,接收方B会告诉发送方A哪些数据丢失,哪些数据已经提前收到,这样TCP只重新发送丢失的包M3,不用发送后续已经正确传输的分组M4。
 TCP首部的选项最多40个字节。 SACK使用了TCP首部的选项: Kind,占1个字节,Kind=5代表是SACK选项; Length,占1个字节,表示SACK整个选项一共占用多少字节。 Left Edge,占4个字节,左边界。 Right Edge,占4个字节,右边界。 最多带4组边界信息,一组占用8个字节,再加上Kind的1个字节,Length的1个字节,所以SACK选项的最大占用字节数是34=4*8+1+1。

客户端从服务端下载大文件:应用层文件很大,会在传输层对文件进行切割,传给网络层。 为什么在传输层进行分割数据,而不是在网络层进行分片传输? 传输层重传可以提高重传的性能。 只有传输层才有可靠传输,也就是可靠传输是在传输层进行控制。 如果在传输层不分段,一旦出现数据丢失,整个传输层的数据都得重传。 如果在传输层分了段,一旦出现数据丢失,只需要重传丢失的那些段即可。

TCP流量控制:

问题: 如果接收方的缓存区满了,发送方还在疯狂发送数据,接收方只能把收到的数据包丢掉。 解决:大量丢包极大的浪费网络资源,所以要进行流量控制。让发送方的发送速率不要太快,让接收方来得及接收处理。发送方和接收方之间的问题,点对点。

原理:通过确认报文中窗口Window字段来控制发送方的发送速率,Window字段告诉对方下一次允许发送的数据大小,字节为单位。发送方发送窗口大小,不能超过接收方给出的窗口大小。当发送方收到接收窗口大小为0的时候,发送方就会停止发送数据。

特殊情况: 一开始,接收方给发送方发送了0窗口的报文段。后面,接收方又有了一些存储空间,给发送方发送的非0窗口的报文段丢失了。发送方的发送窗口一直为0,双方陷入僵局。 解决:当发送方收到0窗口通知时,这个时候发送方停止发送报文。并且同时开启一个定时器,隔一段时间就发个测试报文去询问接收方最新的窗口大小。如果接收的窗口大小还是0,则发送方再次刷新启动定时器。

拥塞控制: 防止过多的数据注入到网络中。避免网络中的路由器或链路过载。 拥塞控制是一个全局性的过程,涉及到所有主机、路由器,以及降低网络传输性能有关的所有因素,大家共同努力的结果。相比,流量控制是点对点通信的控制。

拥塞控制方法:慢开始slow strart,拥塞避免congestion avoidance,快重传fast retransmit,快恢复fast recovery。

MSS,Maximum Segment Size:每个段最大的数据部分大小,在建立连接的时候确定。 cwnd,congestion window:拥塞窗口。 rwnd,receive window:接收窗口。 swnd,send window:发送窗口,是拥塞窗口和接收窗口的最小值swnd = min(cwnd, rwnd)。

慢开始slow start:cwnd的初始值比较小,然后随着数据包被接收方确认,收到ACK,cwnd就会成倍增长,指数级。

拥塞避免congestion avoidance:ssthresh, sow start threshold,慢开始阈值,cwnd达到阈值后,以线性方式增加。使用加法增大,拥塞窗口慢慢增大,以防止网络过早出现拥塞。 当增大到一定程度,就是网络拥塞,这个时候需要使用乘法减小,把ssthresh减半。之后,新旧版本处理方式不同。旧版本,会同时执行慢开始算法,cwnd又恢复到初始值。新版本,会使用快恢复,在阈值的地方使用加法增大,而不是回到初始值。 当网络出现频繁拥塞时,ssthresh值就下降的很快。

如何知道网络拥塞? 当发送方连续收到三个重复确认,就说明网络拥塞,就执行乘法减小算法,把ssthresh减半。 与慢开始不同之处是不执行慢开始算法,也就是cwnd不恢复到初始值,而是把cwnd值设置为ssthresh减半后的值。然后开始执行拥塞避免算法,加法增大,使拥塞窗口缓慢地线性增大。

连接管理:建立连接,三次握手、释放连接,四次挥手。 三次握手建立连接:客户端发送序号SYN=1,确认号ACK=0给服务端;服务端回复TCP序号SYN=1,确认号ACK=1给;客户端回复序号SYN=0,确认号ACK=1。前两次序号SYN都是1。

序号就是当前发送的包第多少个字节。ACK是告诉对方从哪个序号开始发送。 原生值和相对值,发送的实际是原生值,相对值是计算出来的。

原生值的情况: 首先是三次握手,第一步,客户端发送序号seq=s1, ack=0 给服务器,s1是客户端生成随机值。 第二步,服务器回应序号seq=s2,ack=s1+1 给客户端,s2是服务器生成随机值,ack确认的是上一步客户端的发送,也是告诉客户端下一次发送的序号。 第三步,客户端发送seq=s1+1,ack=s2+1 给服务器,数据长度为0。连接建立完成。 第四步,客户端发送数据seq=s1+1,ack=s2+1 给服务器,和第三步不一样的是有数据长度k,都是回复的第二步,所以seq和ack一样。 第五步,服务器发送数据M1、M2、M3给客户端。发送M1数据seq=s2+1,ack=s1+k+1,数据长度b1。发送M2数据seq=s2+b1+1,ack=s1+k+1,数据长度b2。发送M3数据seq=s2+b1+b2+1,ack=s1+k+1,数据长度b3。 第六步:客户端发送seq=s1+k+1,ack=s2+b1+b2+b3+1。客户端ack确认上一次服务器的发送,上一次服务器的seq+数据长度b3+1。

TCP-建立连接-状态: 首先客户端Client处于关闭Closed状态,服务器Server处于监听Listen状态,等待Client连接。 客户端发起连接请求 SYN=1,ACK=0,seq=x,此时客户端进入同步已发送SYN-SENT状态。 服务器接收到客户端的连接请求,服务器进入同步已接收 SYN-RCVD状态。服务器发送连接请求确认 SYN=1,相对ACK=1,seq=y,真实ack=x+1给客户端。 客户端收到服务器确认,并发送收到确认 ACK=1,seq=x+1,ack=y+1 给服务器,客户端进入连接已经建立established 状态。服务器收到确认也进入连接已经建立established 状态。

建立连接前两次握手:SYN都是1,TCP头部的长度一般是32字节。数据部分的长度前三次都是0。 TCP头部固定是20字节,选项部分是12字节。 前两次握手会交换确认一些信息:比如MSS,是否支持SACK,Window scale窗口缩放系数等。这些数据都会放在了TCP头部的选项部分中12字节。

为什么要进行三次握手,两次不行吗?三次主要目的是:防止服务端server一直等待,浪费资源。 如果建立连接只需要2次握手,可能会出现情况:假如客户端client发出的一个连接请求报文段1,因为网络延迟,客户端迟迟没有得到回复判断请求1失效,并又发送了一个新的连接请求2正常建立连接,发送数据,释放连接2。在连接释放以后的某个时间连接请求1才到达服务器server,服务器收到失效的连接1,误认为客户端重新发送了一个新的连接请求,于是服务器就向客户端发出确认报文,同意建立连接。如果不采用三次握手,只要服务器发出确认连接,新的连接就建立成功。由于客户端并不想连接服务器,因此不会对服务器的确认做响应,也不会向服务器发送数据。但服务器却认为新的连接已经建立,并一直等待客户端发送数据,这样服务器的资源就浪费了。

采用三次握手,就可以防止这种现象发生,客户端没有想服务器发送确认,服务器收不到确认,就知道客户端并没有要求建立连接。

第三次握手失败,会怎么处理:此时服务器的状态为SYN-RCVD,若等不到客户端的ACK,服务器会重新发送SYN=1,ACK=1包。如果服务器多次重发SYN=1,ACK=1包,都等不到客户端的ACK,就会发送RST包,强制关闭连接。

TCP释放连接,四次挥手:

长连接和短连接的区分:以socket为例,建立连接后立刻close就叫短连接。等一段时间后再close就是长连接。

应用层Application:FTP、HTTP、DNS。报文,用户数据。

域名Domain Name:为了解决IP地址不方便记忆且不能表达组织的名称和性质的问题。最终还是需要知道目标主机的IP地址。 为什么不直接使用域名,放弃IP:IP地址固定4个字节,域名占用数据比较大,浪费流量,增加路由器负担。 根据域名级别:分为顶级域名TLD,Top-level Domain。二级域名。

通用顶级域名General Top-level Domain,gTLD:.com公司,.net网络机构,.org组织机构,.edu教育,.gov政府部门,.int国际组织等。 国际及地区顶级域名 Country Code Top-level Domain,ccTLD:.cn中国,.jp日本,.uk英国。 新通用顶级域名,New Generic Top-level Domain,New gTLD:.vip,.xyz、.top,.club,.shop等 
二级域名:顶级域名之下的域名。在通用顶级域名下,一般指域名注册人的名称,例如google、baidu等。一般域名 baidu.com 是由顶级域名和二级域名组成。baidu 是二级域名,.com是顶级域名。 从右向左数,第一个是顶级域名,第二个.是二级域名,依次三级四级。

DNS,Domain Name System:利用DNS协议,将域名解析成对应的IP地址。DNS可以基于UDP协议,也可以基于TCP协议,服务器占用53端口。

ipconfig /displaydns: 查看DNS缓存记录 ipconfig /flushdns: 清空DNS缓存记录 ping 域名 nslookup 域名

DNS服务器:访问www.baidu.com,客户端首先访问最近的一台DNS服务器(客户端自己配置的DNS服务器),然后这台最近的DNS服务器会请求根域名服务器,根域名服务器返回对应顶级域名的DNS服务器IP,包含.com的IP。接着这台最近的DNS服务器向根域名DNS服务器发起请求,返回baidu.com所在的DNS服务器IP,二级域名DNS服务器IP。最后最近的这台DNS服务器向二级域名DNS服务器发起请求获取baidu.com对应的IP。

所有DNS服务器都记录了DNS根域名服务器的IP地址。 上级DNS服务器记录了下一级DNS服务器的IP地址。 全球一共13台IPv4的DNS根域名服务器、25台IPv6的DNS根域名服务器。

IP分配方式分为:静态IP地址、动态IP地址。 动态IP地址:从DHCP服务器自动获取IP地址。移动设备、无限设备等。

DHCP,Dynamic Host Configuration Protocol,动态主机配置协议。基于UDP协议,客户端端口68,服务器端口67。DHCP服务器会从IP地址池中挑选一个IP地址给客户端一段时间,时间到期就会回收。 家里上网的路由器就充当了DHCP服务器。

DHCP分配IP地址的4个阶段:discover,发现服务器。发广播包,源IP是0.0.0.0,目标IP是255.255.255.255,目标MAC地址是FF:FF:FF:FF:FF:FF;offer提供租约。服务器返回可以租用的IP地址,以及租用期限,子网掩码、网关、DNS等信息。可能会有多个服务器提供租约;request,选择IP地址。客户端选择一个offer,发送广播包进行回应;ackNowledge,确认。被选中的服务器发送ACK数据包给客户端,IP地址分配完毕。

DHCP服务器可以跨网段分配IP地址吗?DHCP服务器、客户端不在同一个网段:可以借助DHCP中继代理,DHCP Relay Agent 实现跨网段分配IP地址。

自动续约:客户端会在租期不足的时候,自动向DHCP服务器发送Request信息申请续约。 ipconfig /all : 可以看到DHCP相关的详细信息,比如租约过期时间、DHCP服务器地址等。 ipconfig /release: 释放租约。 ipconfig /renew: 重新申请IP地址、申请续约(延长租期)。

HTTP,Hyper Text Transfer Protocol,超文本传输协议。最初用来发布和接收HTML,使用URI标识具体资源。 HTML,Hyper Text Markup Language ,超文本标记语言。

1991年HTTP/0.9只支持GET获取文本数据HTML。 1996年HTTP/1.0支持POST、HEAD等请求,支持请求头、响应头,支持更多种数据类型,不再局限于文本数据,并且浏览器每次请求需要与服务器建立一个TCP连接,请求处理完毕立即断开TCP连接。 1997年HTTP/1.1,最经典、使用最广泛的版本,支持PUT、DELETE等请求方式,采用持久连接Connection: keep-alive,多个请求可以共用同一个TCP连接。 2015年HTTP/2.0 2018年HTTP/3.0

HTTP标准RFC,Request For Comments,请求意见稿。使用ABNF语言描述HTTP请求格式。 报文格式:0A换行CR,0D回车LF,20空格。

请求行 -> 请求头 -> 请求体。

GET:常用于读取的操作,请求参数直接拼接在URL的后面,浏览器或服务器会对URL长度有限制。 POST:常用于添加、修改、删除的操作,请求参数可以放在请求体中,没有大小限制。 HEAD:请求得到与GET请求相同的响应,但没有响应体。使用场景:在下载一个大文件前,先获取其大小,再决定是否要下载,以此可以节约带宽资源。 OPTIONS:用于获取服务器(目的资源)所支持的通信选项,比如服务器支持的请求方法。OPTIONS * HTTP/1.1 。

PUT:用于对已存在的资源进行整体覆盖。 PATCH:用于对资源进行部分修改,资源不存在会创建新资源。 DELETE:用于删除指定的资源。 TRACE:请求服务器回显其收到的请求信息,主要用于HTTP请求的测试或诊断。 CONNECT:可以开启一个客户端与所请求资源之间的双向沟通的通道,可以用来创建隧道tunnel,可以用来访问采用了SSL/HTTPS协议的站点。

头部字段Header Field,分为四种:请求头字段 Request Header Fields、响应头字段 Response Header Fields、实体头字段 Entity Header Fields、通用头字段 General Header Fields。实体头字段 、通用头字段在请求头和响应头字段中包含。

请求头字段 Request Header Fields:User-Agent 浏览器的身份标识字符串;Host 服务器的域名、端口号;Date 发送该消息的日期和时间;Content-Type 请求体的类型;Content-Length 请求体的长度,单位字节;Referer:表示浏览器所访问的前一个页面。也就是这个页面从哪个页面点击跳转过来的,可以用来防止倒链,防止图片倒链;Accept 能够接受的响应内容类型 Content-Types,Accept: text/plain;Accept-Charset 能够接受的字符集, Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7 使用逗号隔开,q是权重优先级,没有设置是1.0最大;Accept-Encoding 能够接受的编码方式列表,Accept-Encoding: gzip, deflate;Accept-Language 能够接受的响应内容的自然语言列表,Accept-Language: en-US;Range 仅请求某个实体的一部分,字节偏移以0开始,Range: bytes=500-900,多线程断点下载;Connection 该浏览器想要优先使用的链接类型,Connection: keep-alive;

响应头字段 Response Header Fields:Date 发送该消息的日期和时间;Server 服务器的名字;Content-Type 响应体的类型,Content-Type: text/html; charset=UTF-8;Content-Encoding 内容所使用的编码类型 Content-Encoding: gzip;Content-Length 响应体的长度,字节为单位,Content-Length: 348;Content-Disposition 指定数据是文件类型,不要在浏览器中展示,直接进行下载,可以指定文件名,Content-Disposition: attachment; filename=“fname.jpg” ;Connection 针对该连接所预期的选项,Connection: close ;

状态码 Status Code 分为五类:信息响应100199,成功响应200299,重定向300399,客户端错误400499,服务器错误500~599。

100 Continue :服务器会根据请求头来判断是否接受客户端的请求,客户端先发送请求头,服务端判断接收请求,返回100 Continue,然后客户端再发送请求体,服务端接收请求体。 200 OK:请求成功。 302 Found:重定向,请求的资源被暂时移动到了由Location头部指定的URL上。 304 Not Modified:不需要再次传输请求的内容,也就是可以使用缓存的内容。 400 Bad Request:由于语法无效,服务器无法理解该请求。 401 Unauthorized:由于缺乏目标资源要求的身份验证凭证。 403 Forbidden:服务器端有能力处理该请求,但是拒绝授权访问。 404 Not Found:服务器端无法找到所请求的资源。 405 Method Not Allowed:服务器禁止了使用当前HTTP方法的请求。 406 Not Acceptable:服务器端无法提供与Accept-Charset以及Accept-Language 指定的值相匹配的响应。

500 Internal Server Error:所请求的服务器遇到意外的情况并阻止其执行请求。 501 Not Implemented:请求的方法不被服务器支持,因此无法被处理,服务器必须支持的方法只有GET和HEAD,也就是只有GET和HEAD方法不会返回这个状态码。 502 Bad Gateway:作为网关或代理角色的服务器,从上游服务器中接收的响应是无效的。 503 Service Unavailable:服务器尚未处于可以接受请求的状态,由于服务器停机维护或已超载。

form提交,常用属性: action,请求的URI;method,请求方法,GET、POST;enctype,POST请求时,请求体的编码方式,有application/x-www-form-urlencoded默认值,会用&分隔参数,用=分隔键和值,字符用urlencoded进行编码。还有文件上传必须使用multipart/form-data,变化是请求头Content-Type:multipart/form-data;boundary=,请求体会变复杂;

跨域问题的出现解决的问题:为了安全,不允许任意一个请求来源都能访问服务器资源。

请求头的 Origin 和响应头里面的 Access-Control-Allow-Origin 用于跨域:浏览器有个同源策略 Same-Origin Policy,规定默认AJAX异步请求只能发送同源的URL,同源指协议、域名/IP、端口相同。img、script、link、iframe、video、audio等标签不受同源策略影响。

解决AJAX跨域请求的常用方法:CORS,Cross-Origin Resource Sharing,跨域资源共享。CORS的实现需要客户端和服务器同时支持。客户端所有浏览器都支持,IE10以上。服务器需要返回响应头Access-Control-Allow-Origin,告诉浏览器这个请求允许跨域访问,也就是指定哪些来源网站可以跨域访问这个资源,Access-Control-Allow-Origin: *。 请求头Origin:告诉服务器我发起的请求的源头是什么,Origin:www.baidu.com。后端可以更加这个判断这个请求是否可以跨域。

请求头的Cookie和响应头里面的 Set-Cookie :HTTP是无状态的,每次请求是独立的,无法判断每个请求是否来自同一个浏览器。 Cookie和session是一起使用的,Cookie是存储在浏览器,session存储在服务器。 Set-Cookie: JSESSIONID=; 浏览器关闭 或者到了时间Cookie就会清除。请求中如果没有JSESSIONID 服务器就会创建新的Session对象,如果有JSESSIONID就会直接获取到Session对象。如果请求的cookie数据服务器判断有问题,就可以返回302重定向Location到登录页面。

代理服务器 Proxy Server :转发上下游的请求和响应,对于下游是服务器,对于上游是客户端。 正向代理:代理的对象是客户端。反向代理:代理的对象是服务器。 正向代理作用:隐藏客户端身份、绕过防火墙、访问权限控制、数据过滤。Fiddler、Charles等抓包工具是在客户端启动了正向代理服务。 反向代理作用:隐藏服务器身份、安全防护、负载均衡。

Wireshark的原理:通过底层驱动,拦截网卡上流过的数据。

网络安全:截获、中断、篡改、伪造。 网络层的ARP欺骗,ARP攻击: DoS攻击,拒绝服务攻击 Denial-of-Service attack:使目标电脑的网络或系统资源耗尽 ,使服务暂时中断或停止,导致其他正常用户无法访问。 DDoS攻击,分布式拒绝服务攻击,Distributed Denial-of-Service attack:黑客使用网络上两个或以上被攻陷的电脑作为僵尸/肉鸡向特定目标发动DoS攻击。

DoS攻击分为两类: 带宽消耗型:UDP洪水攻击、ICMP洪水攻击。 资源消耗型:SYN洪水攻击、LAND攻击。 SYN洪水攻击:攻击者发送一些列的SYN请求到目标,然后让目标因收不到ACK第3次握手而进行等待、消耗资源。 LAND攻击:。

防御方式使用防火墙:设置规则,拒绝特定通讯协议、端口或IP地址;拒绝攻击源IP发出的通信; 防火墙比较靠后,恶意攻击流量在到达防火墙之前,在到达路由器的时候已经被攻击。

应用层 DNS劫持:篡改了域名解析结果。使用比较靠谱的DNS服务器 114.114.114.114 。 HTTP劫持:对HTTP数据包进行拦截处理,比如插入JS代码,莫名的出现弹窗广告。

HTTP协议的安全问题:采用明文传输,有很大安全隐患。 提高安全的方法:对通信内容进行加密后再传输。

encrypt 加密、decrypt 解密、plaintext 明文、ciphertext 密文。 单向散列函数 One-way hash function:可以根据消息内容计算出散列值。散列值的长度和消息的长度无关,无论消息多大10M还是100G,都能计算出固定长度的散列值。单向不可逆。 也叫消息摘要函数 message digest function,哈希函数 hash function。输出的散列值也叫消息摘要 message digest,指纹 fingerprint。

单向散列函数的应用:防止数据被篡改;密码加密。

AES,Advanced Encryption Standard:取代DES的对称加密算法。长度有128、192、256bit三种。

如何解决对称秘钥配送问题:事先共享秘钥;秘钥分配中心;非对称加密 Asymmetric Cryptography;

非对称加密 Asymmetric Cryptography:公钥 public key、私钥 private key。加密解密速度比较慢。有:RSA。

A、B发送消息,A下发自己的公钥给B,B下发自己的公钥给A。当A给B发送消息,就使用自己的私钥加密消息,B收到后使用A下发的公钥解密,消息可能被其他人获取,但无法被篡改。B给A发送消息同样使用B自己的私钥。

混合密码系统:结合对称加密和非对称加密。为什么要结合使用:非对称加密效率不高,比较慢,但安全,对称加密快,但不安全,所以结合起来使用。

会话密钥 session key: 为某次通信随机生成的临时对称秘钥,用于加密消息,对称秘钥提高速度。

非对称加密步骤:首先,消息发送者获取消息接受者的公钥。然后生成会话密钥,对称密钥,加密消息。用消息接收者的公钥加密会话密钥。将前面生成的加密消息、公钥加密的会话密钥一起发送给消息接收者。 解密步骤:消息接收者使用公钥解密出会话密钥,然后使用会话密钥解密加密消息。

问题:消息接收者如何确定消息的真实性,如何识别篡改、伪装、否认? 解决方案:数字签名。

既然是加密,肯定是不希望别人知道我的消息,所以只有我才能解密。公钥负责加密,私钥负责解密。

数字签名过程: 生成签名:消息发送者通过 “签名密钥/私钥” 生成。 验证签名:消息接收者通过 “验证密钥/公钥” 验证。 如何保证这个签名是消息发送者自己签的?用消息发送者的私钥进行签名。

数字签名的过程: 消息发送者A使用自己的私钥对消息摘要进行加密,这个加密内容就是签名,发送消息、签名、公钥给消息接收者B。消息接收者B获取到消息,获取消息摘要,使用A的公钥解密签名,然后比对摘要是否相同。 签名解决的问题是:确认消息的完整性,识别消息是否被篡改,防止消息发送人否认。 数字签名中,任何人都可以使用公钥进行验证签名。

对消息摘要进行非对称加密,而不是直接对消息进行非对称加密。

既然是签名,肯定是不希望有人冒充我发消息,所以只有我才能签名。私钥负责签名,公钥负责验签。

非对称加密过程,公钥的合法性问题:遭到中间人攻击,公钥可能被伪造。 解决:证书 Certificate。公钥证书 Public-key Certificate,PKC,跟驾驶证类似。 包含姓名、邮箱等个人信息,以及公钥。由认证机构 Certificate Authority,CA添加数字签名,使用CA的私钥进行添加的数字签名。

CA就是能够认定 “公钥确实属于此人” 并能够生成数字签名的个人或组织。

证书的使用流程:消息接收者B生成秘钥对,B从CA注册自己的公钥,然后CA使用自己的私钥对B的公钥施加数字签名并生成证书。消息发送者A从CA获取证书,带有数字签名和B的公钥。A使用CA的公钥验证数字签名,确认B的公钥的合法性。然后A用B的公钥加密消息并发送给B。B接收到消息使用自己的私钥解密得到A的消息。

证书的注册和下载流程: 消息接受者B,也就是公钥注册者,拿着自己的公钥去CA注册。CA使用自己的私钥对B的公钥进行签名认证,生成证书。证书包括B的公钥、CA生成的数字签名、B的信息。证书保存到证书仓库,消息发送者A,也就是证书使用者,去证书仓库下载证书。

CA 的公钥,默认已经内置在浏览器和操作系统中。

HTTPS,HyperText Transfer Protocol Secure:默认端口443。在HTTP基础上加上 SSL/TLS 安全套接层,防止窃听和中间人攻击。

TLS,Transport Layer Security,前身是SSL,Secure Sockets Layer。SSL/TLS还可以用在其他协议上:FTPS、邮件协议 SMTPS。SSL/TLS 工作在应用层和传输层之间。SSL/TLS 分为:Handshake Layer层、Record Layer层。

OpenSSL是SSL/TLS协议的开源实现。可以使用OpenSSL构建一套自己的CA,自己给自己颁发证书,自签名证书。

生成私钥:openssl genrsa - out my.key 生成公钥:openssl rsa -in my.key -pubout -out my.pem

HTTPS 的通信过程,分为三大阶段: 1、TCP三次握手 2、TLS的连接 3、HTTP请求和响应

HTTP是明文传输。HTTPS就是在HTTP基础上加了,即HTTP->TLS/SSL->TCP->IP。https可以防止信息窃听、篡改、劫持,对信息进行加密、完整性校验和身份验证。https标准端口443。https基础传输层,http是基于应用层。

HTTPS主要作用:对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;对网站服务器进行真实身份认证;

TCP:是面向连接、可靠的字节流服务。建立TCP连接需要三次握手。可靠基础是提供了:超时重发、丢弃重复数据、检验数据、流量控制,保证数据能从一端到另一端。 UDP:用户数据报协议,面向数据报的运输层协议。面向非连接的协议,不用与对方建立连接,直接把数据包发送过去。不需要建立连接,传输数据量少,没有超时重发机制,所以传输速度很快。

TCP三次握手:第一次,客户端发送syn包到服务器,并进入SYN_SEND状态,等待服务器确认;第二次,服务器收到syn包,必须确认客户的SYN,ack=j+1,同时自己也要发送一个SYN包,syn=k,即SYN+ACK包,此时服务器进入SYN_RECV状态;第三次,客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK,ack=k+1,此包发送完毕,客户端和服务器进入established建立状态,完成三次握手。

客户端、服务端都可以主动发起断开TCP连接的请求。断开连接的四次挥手。

Socket:是一套完成TCP,UDP协议的接口。http是基于socket之上的。HTTP协议是基于TCP链接的。tcp协议:对应于传输层。IP协议:对应于网络层。TCP/IP主要解决数据在网络中如何传输;HTTP是应用层协议,主要解决如何包装数据。 Socket:是对TCP/IP协议的封装,Socket本省并不是协议,而是一个调用接口,通过Socket,我们才能使用TCP/IP协议。

HTTP连接:短连接,客户端向服务端发送一次请求,服务器响应后,主动释放连接。 Socket连接:长连接,客户端和服务端建立连接后,非异常情况不会断掉。异常情况有:服务端或客户端异常崩溃,网络故障,长时间没有数据传输,网络防火墙可能断开连接释放网络资源。所以当一个socket长连接中没有数据的传输时,为了维持连接需要发送心跳消息。

Socket建立网络连接的步骤:建立socket连接需要一对socket,一个运行在客户端ClientSocket,一个运行在服务端ServerSocket。第一,服务器监听,ServerSocket处于等待连接的状态,实时监控网络状态,等待客户端的链接请求;第二,客户端发送请求,ClientSocket连接服务端ServerSocket。客户端需要描述要连接的ServerSocket,包括地址、端口号;第三,连接确认,当ServerSocket监听到ClientSocket的连接请求,就响应客户端请求,建立一个新的线程,把ServerSocket的描述发送给ClientSocket,一旦双方确认了此描述,就正式建立连接。而ServerSocket继续处于监听状态,继续接收其他ClientSocket的连接请求;

问题:IM软件能够长时间在线,或者短时间内掉线,最好可以用户无感知。让IM软件维持在线的状态。用到了断线重连机制。 Socket 编程中的断线重连机制,GCDAsyncSocket,实现: IM软件能够始终尽可能的保持跟服务器的链接,客户端维护已登录状态,以便断线重连。从逻辑层次上来说,断线重连的逻辑是基于登录逻辑的,首次登录成功后,都有可能断线重连。步骤:第一,使客户端断线;第二,让客户端重连服务器。

客户端断线重连服务器情况:网络连接失败,网络不可用,收到iOS系统”网络可用”的通知;网络切换wifit->4G/5G情况;心跳失败,心跳超时,说明当前客户端和服务器连接已经损坏,或当前用户身份有变化。心跳失败后首先将客户端离线,然后进行断线重连操作,避免心跳失败和网络错误事件一并发生,造成两次登录;客户端重新启动,提前加载用户缓存的最近会话数据;IM软件后台运行被系统终止的情况,切换到前台;为了避免重复登录,在网络可用或者切换到前台的时候,IM处于登录成功、连接中、已注销等状态时,客户端不要重新连接。

心跳:客户端定时向Server发一个信令包,表示客户端还活着。心跳终止:1、Server主动断开socket,Server只接收客户端发起的心跳。Server长时间没有收到客户端的心跳,认为客户端已死,主动断开连接。此时客户端可能就是假在线;2、客户端断开socket,客户端多长时间发送一次心跳,客户端发送一次心跳,服务端长时间没有回应,网络不好,客户端需要主动离线,超时时间一般60秒。

远程推送APNS的过程:1、客户端将用户UUID和app的bundleID发送给APNS服务器,进行注册,ANPS将加密后的device Token返回给app;2、app获得device Token后,上传到公司服务器;3、当需要推送通知时,公司服务器会将推送内容和device Token 一起发送给APNS服务器;4、APNS再将推送内容推送到相应的客户端app上;

GET产生一个TCP数据包;POST产生两个TCP数据包,GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。