那些常见的算法“面筋”I

文章详细介绍了如何使用快慢指针判断链表是否有环,以及LRU缓存淘汰算法的原理和四种编程语言(Java,Python,C++,Go)的实现。同时,文章还讨论了其他算法问题,如数组中第K大元素、K个一组翻转链表和三数之和的最大子数组和的解法,包括动态规划和分治策略的应用。
摘要由CSDN通过智能技术生成

1. 怎么判断链表是否有环

2. LRU算法

3. 数组中第K大个元素

4. K个一组翻转链表

5. 三数之和

6. 最大子数组和

一 怎么判断链表是否有环

        快慢指针法:定义快慢指针,快指针每次走两步,慢指针每次走一步,如果无环,那么快指针会提前到底终点,否则,快慢指针会相遇。有一个非常简单的证明方法,但慢指针进入环时,假设快慢指针移动方向上距离K,那么另一端方向距离就是N-K(N为环长度),那么在移动方向上K是按+1变大的因为快指针比慢指针快一步,那么N-K就是按-1变小的,那么N-K终究会变为0,即快慢指针终会相遇。

        实现代码(java):

public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head.next;
    while (slow != fast) {  // slow=fast就相遇了,立即跳出
        // 在首次判断后,快指针始终比慢指针快,所以只要判断快指针是否为空即可,        
        // 另外因为快指针是跳两步的,那么避免报错,next也要遍历的
        if (fast == null || fast.next == null) {  // 到这里说明快指针到终点了,无环
            return false;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}

        实现代码(Python):

def hasCycle(head):
    if not head or not head.next:  // None也一样
        return false
    slow, fast = head, head.next
    while slow != fast:
        if not fast or not fast.next:
            return False
        slow = slow.next
        fast = fast.next.next
    return True

        实现代码(C++):

bool hasCycle(ListNode *head) {
    if (head == nullptr || head->next == nullptr) {   // C++11新特性,用nullptr较为好
        return false;
    }
    ListNode *slow = head;
    ListNode *fast = head->next;
    while (slow != fast) {
        if (fast == nullptr || fast->next == nullptr) {
            return false;
        }
        slow = slow->next;
        fast = fast->next->next;
    }
    return true;
}

        实现代码(Go):

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

二 LRU算法

        一种缓存淘汰算法,核心思想:当缓存空间满时,优先淘汰掉最近最少使用的缓存数据。

        描述:维护一个缓存数据的访问时间列表,每次访问缓存数据时,将该数据的访问时间更新到列表的末尾;当缓存空间满时,淘汰掉访问时间最早的缓存数据。

        实现:

  1. 定义一个双向链表和一个哈希表,前者维护缓存数据的访问顺序,后者快速查找缓存数据。

  2. 当有访问缓存数据时, 在hash表中查找是否存在,若存在,则解链表,把该节点删除原位置后放首位,并返回值,若不存在,则hash表中添加并在链表首项添加(hash的k-v即缓存key-链表节点)。

  3. 空间满删除时,把链表尾数据依次删除,并据链表存的hash的key去删除hash表中的数据。

        代码实现(java):

class LRUCache {
    class ListNode {
        int key;
        int value;
        ListNode pivot;
        ListNode next;
        ListNode() {}
        ListNode(int key, int value) {this.key=key; this.value=value;}
    }
    int capacity = 0;
    HashMap<Integer, ListNode> map;
    ListNode head = new ListNode();
    ListNode tail = new ListNode();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head.next = tail;
        tail.pivot = head;
    }
    
    public int get(int key) {
        if (map.containsKey(key)) {
            ListNode node = map.get(key);
            removeHead(node);
            return node.value;
        } else {
            return -1;
        }
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            ListNode node = map.get(key);
            node.value = value;
            removeHead(node);
        } else {
            if (capacity == map.size()) {
                map.remove(deleteTail());
            }
            ListNode node = new ListNode(key, value);
            map.put(key, node);
            makehead(node);
        }
    }

    private void removeHead(ListNode node) {
        deleteNode(node);
        makehead(node);
    }

    private int deleteTail() {
        ListNode node = tail.pivot;
        deleteNode(node);
        return node.key;
    }
    
    private void deleteNode(ListNode node) {
        node.pivot.next = node.next;
        node.next.pivot = node.pivot;
    }

    private void makehead(ListNode node) {
        node.next = head.next;
        node.pivot = head;
        head.next.pivot = node;
        head.next = node;
    }
}

       代码实现(Python):

class LinkNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache(object):

    def __init__(self, capacity):
        self.map = dict()  # 创建hash map
        self.head = LinkNode()
        self.tail = LinkNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        self.capacity = capacity

    def get(self, key):
        if key in self.map:
            node = self.map[key]
            self.removeHead(node)
            return node.value
        else:
            return -1

    def put(self, key, value):
        if key in self.map:
            node = self.map[key]
            node.value = value
            self.removeHead(node)
        else:
            if self.capacity == len(self.map):
                self.map.pop(self.deleteTail())

            node = LinkNode(key, value)
            self.map[key] = node
            self.makehead(node)

    def removeHead(self, node):
        self.deleteNode(node)
        self.makehead(node)

    def deleteTail(self):
        node = self.tail.prev
        self.deleteNode(node)
        return node.key

    def deleteNode(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def makehead(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

        代码实现(C++):

class LRUCache {
private:
    struct ListNode {
        int key;
        int value;
        ListNode* pivot;
        ListNode* next;
        ListNode() : key(0), value(0), pivot(nullptr), next(nullptr) {}
        ListNode(int k, int v) : key(k), value(v), pivot(nullptr), next(nullptr) {}
    };
    int capacity;
    unordered_map<int, ListNode*> map;
    ListNode* head;
    ListNode* tail;

public:
    LRUCache(int capacity) {
        this->capacity = capacity;
        head = new ListNode();
        tail = new ListNode();
        head->next = tail;
        tail->pivot = head;
    }

    int get(int key) {
        if (map.count(key) == 0) {
            return -1;
        }
        ListNode* node = map[key];
        removeHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (map.count(key) > 0) {
            ListNode* node = map[key];
            node->value = value;
            removeHead(node);
        } else {
            if (map.size() == capacity) {
                map.erase(deleteTail());
            }
            ListNode* node = new ListNode(key, value);
            map[key] = node;
            makehead(node);
        }
    }

private:
    void deleteNode(ListNode* node) {
        node->pivot->next = node->next;
        node->next->pivot = node->pivot;
    }

    void makehead(ListNode* node) {
        node->next = head->next;
        node->pivot = head;
        head->next->pivot = node;
        head->next = node;
    }

    void removeHead(ListNode* node) {
        deleteNode(node);
        makehead(node);
    }

    int deleteTail() {
        ListNode* node = tail->pivot;
        deleteNode(node);
        int key = node->key;
        delete node;  // C++操作防止内存泄漏
        return key;
    }
};

        代码实现(Go):

type LRUCache struct {
    size int
    capacity int
    cache map[int]*LinkNode
    head, tail *LinkNode
}

type LinkNode struct {
    key, value int
    prev, next *LinkNode
}

func initLinkNode(key, value int) *LinkNode {
    return &LinkNode{
        key: key,
        value: value,
    }
}

func Constructor(capacity int) LRUCache {
    l := LRUCache{
        cache: map[int]*LinkNode{},
        head: initLinkNode(0, 0),
        tail: initLinkNode(0, 0),
        capacity: capacity,
    }
    l.head.next = l.tail
    l.tail.prev = l.head
    return l
}

func (this *LRUCache) Get(key int) int {
    if _, ok := this.cache[key]; ok {
        node := this.cache[key]
        this.removeHead(node)
        return node.value
    } else {
        return -1
    }
}

func (this *LRUCache) Put(key int, value int) {
    if node, ok := this.cache[key]; ok {
        node.value = value
        this.removeHead(node)
    } else {
        if len(this.cache) == this.capacity {
            delete(this.cache, this.deleteTail())
        }
        node := initLinkNode(key, value)
        this.cache[key] = node
        this.makehead(node)
    }
}

func (this *LRUCache) makehead(node *LinkNode) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
}

func (this *LRUCache) removeNode(node *LinkNode) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (this *LRUCache) removeHead(node *LinkNode) {
    this.removeNode(node)
    this.makehead(node)
}

func (this *LRUCache) deleteTail() int {
    node := this.tail.prev
    this.removeNode(node)
    return node.key
}

三 数组中的第K个最大元素

        来源于一经典问题:线性时间选择。

        描述:基于快速排序思想对分开区域不做排序且中位索引随机方式查找,一首先分区,随机生成中位索引,在范围[l, r]中,把索引值X记录并与r位置交换,遍历l->r-1,i从l开始,将比x小的值都记录到i中,最后再将i的最后索引位与r交换,返回i值,得到以i索引为中心的两边大小分区;二递归查找,根据分区大小判断是否找到第K大,否则按长度进入对应分区寻找第K大。

        代码实现(java):

class Solution {
    public int findKthLargest(int[] nums, int k) {
        return quickSelect(nums, 0, nums.length - 1, nums.length - k);
    }
    
    private int quickSelect(int[] nums, int l, int r, int k) {
        int q = select(nums, l, r);
        if (q == k) {
            return nums[q];
        } else {
            return k < q ? quickSelect(nums, l, q - 1, k) : quickSelect(nums, q + 1, r, k);
        }
    }

    private int select(int[] nums, int l, int r) {
        int randomIndex = (new Random()).nextInt(r - l + 1) + l;
        swag(nums, randomIndex, r);  // 现在r的位置放的是random
        int mid = l - 1;
        for (int i = l; i < r; i++) {
            if (nums[i] < nums[r]) {
                swag(nums, i, ++mid);
            }
        }
        swag(nums, ++mid, r);  
        return mid;
    }

    private void swag(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

四 K 个一组翻转链表

        想法1:因为不能直接读到链表长度,那么计数即可,读到链表节点数到k,则反转,直接节点为空暂停遍历一个节点A记录链表每次的头,另一个节点B记录链表尾,然后最后节点B的下一位进入循环,用栈做辅助,后进先出,巧好移动A达到反转k个的目的(思路可以,但最终的速度和空间都不太行)

        优化 ——>  链表断开反转,再接上,不过每次反转,都需要在反转的链表头加一个辅助节点,且每次走k个点时头和尾要同起点,另外由于原头节点也会被反转,所以还需要一个全局额外的头节点,以返回结果。

         代码实现(java):

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode root = new ListNode(0, head);  // 0 -> (head ->)
        ListNode end = root;
        head = root;  // 从头节点开始
        while (end != null) {
            for (int i = 0; i < k && end != null; i++) {
                end = end.next;
            }
            if (end == null) {
                break;
            }
            ListNode head_ = head.next;
            ListNode end_ = end.next;  // 记录不需反转的后续位置
            end.next = null;  // 这样传入反转链表才不会破坏后面的
            head.next = reverse(head_);
            head_.next = end_;  // 此时的head_就相当于原链表中的end位置
            // head和end往下一个循环走,同起点,且不触及下一个会使用的节点
            head = head_;
            end = head_;
        }
        return root.next;
    }

    // 反转链表,cur-root-next一气呵成,cur、root是操作核心,next是记录后者
    public ListNode reverse(ListNode root) {
        ListNode cur = null;  
        while (root != null) {
            ListNode next = root.next;
            root.next = cur;
            cur = root;
            root = next;
        } 
        return cur;
    }
}

五 三数之和

        核心:排序 + 双指针法

        注意一个重要的限制条件,即三元组不重复。

        描述:双指针需要先排序,设i: 0->len-3遍历,由后面两个指针搜索达到sum=0值数,显然排序后,i必然是最下的那个,即nums[i]必然<=0,故可以把nums[i]>0作为一移动条件。当遍历i后,若nums[i]与nums[i-1]相同,那么跳过,因为nums[i-1]会遍历出对应的情况【三元组不重复】。然后l=i+1 - r=len-1开始搜寻,只要l<r,那么在sum>0时左移,sum<0时右移。

        代码实现(java):

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length - 2; i++) {
            if (nums[i] > 0) return res;  
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            int l = i + 1;
            int r = nums.length - 1;
            while (l < r) {
                int sum = nums[i] + nums[l] + nums[r];
                if (sum < 0) {
                    l++;
                } else if (sum > 0) {
                    r--;
                } else {
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[l]);
                    list.add(nums[r]);
                    res.add(list);
                    while (l < r && nums[l] == nums[l + 1]) l++;  // 排掉使用过的相同元素,i固定,那么只要nums[l]不变,nums[r]值必然不变
                    while (l < r && nums[r] == nums[r - 1]) r--;  // 排掉使用过的相同元素
                    l++;  // l+1位还是一样的,需要再往后一位
                    r--;
                }
            }
        }
        return res;
    }
}

六 最大子数组和

动态规划(优):

        初始化:f[0] = nums[0]

        状态转移方程:f[i] = max(f[i - 1] + nums[i] , nums[i]) , i>0  

                      res = max(res, f[i])

        对于任意的nums[i],显然只有前面累计值为正时,才会更大,往前想也如此,状态方程即这样出来的。

代码实现(java):

public int maxSubArray(int[] nums) {
    // dp[i]代表使用nums[i]的连续最大和
    int[] dp = new int[nums.length];
    // 初始化
    dp[0] = nums[0];  
    int res = dp[0];
    
    for (int i = 1; i < nums.length; i++) {
        dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
        res = Math.max(res, dp[i]);
    }
    return res;
}

分治法:

        在不考虑单元素取值的情况下,中间向两边扩散,分治到底选出最大值,分治到最后也会拿单元素比较,但扩缩时以至少两边元素和来做扩散。

public class Solution {
    public int maxSubArray(int[] nums) {
        return maxSubArraySum(nums, 0, nums.length - 1);
    }

    // 计算至少包含 nums[mid]和nums[mid+1] 的最大和(这里是按左右两边累计找和)
    private int maxCrossingSum(int[] nums, int left, int mid, int right) {
        int sum = 0;
        int leftSum = Integer.MIN_VALUE, rightSum = Integer.MIN_VALUE;
        // 左半边包含 nums[mid] 元素,最多可以到什么地方,即计算以 mid 结尾的最大的子数组的和
        for (int i = mid; i >= left; i--) {
            sum += nums[i];
            if (sum > leftSum) {
                leftSum = sum;
            }
        }
        sum = 0;
        // 右半边不包含 nums[mid] 元素,最多可以到什么地方,即计算以 mid+1 开始的最大的子数组的和
        for (int i = mid + 1; i <= right; i++) {
            sum += nums[i];
            if (sum > rightSum) {
                rightSum = sum;
            }
        }
        return leftSum + rightSum;
    }

    private int maxSubArraySum(int[] nums, int left, int right) {
        if (left == right) {
            return nums[left];  // 考虑一个元素的情况最终会落到这里,而不是maxCrossingSum
        }
        int mid = left + (right - left) / 2;
        return max3(maxSubArraySum(nums, left, mid),
                maxSubArraySum(nums, mid + 1, right),
                maxCrossingSum(nums, left, mid, right));  // 一直分治拆到每一个包含的最大和做比较
    }

    private int max3(int num1, int num2, int num3) {
        return Math.max(num1, Math.max(num2, num3));
    }
}
// 例如数组为4大小的数组
// su(0,1), su(2,3) c(0,1,3)
// su(0,0), su(1,1) c(0,0,1)
// su(2,2), su(3,3) c(2,2,3)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值