邻值查找

Posted by zhouqian on Thursday, June 16, 2022

acwing 136. 邻值查找

给定一个长度为 n 的序列 A ,A 中的数各不相同。

对于 A 中的每一个数 Ai ,求:

min1≤j<i|Ai−Aj|

以及令上式取到最小值的 j (记为 Pi )。若最小值点不唯一,则选择使 Aj 较小的那个。

输入格式

第一行输入整数 n ,代表序列长度。

第二行输入 n 个整数A1…An ,代表序列的具体数值,数值之间用空格隔开。

输出格式

输出共 n−1 行,每行输出两个整数,数值之间用空格隔开。

分别表示当 i 取 2∼n 时,对应的 min1≤j<i|Ai−Aj| 和 Pi 的值。

数据范围

n≤1e5 ,|Ai|≤1e9


方案一:结合链表和数组,数组保存链表节点指针地址,用作 O(1) 时间查找链表,优化链表的查找较慢的问题。

package main

import (
    "fmt"  
    "sort"
)

// node 双向链表节点。
type node struct {
    idx  int
    val  int
    pre  *node
    next *node
}

// addNode 在 head 后插入节点 newNode。
func addNode(head, newNode *node) {
    newNode.next = head.next
    newNode.pre = head
    head.next.pre = newNode
    head.next = newNode
} 

// deleteNode 删除节点。
func deleteNode(node *node) {
    node.pre.next = node.next
    node.next.pre = node.pre
}

func main() {
    // 保存输入的数组长度
    var n int
    fmt.Scanf("%d", &n)
    
    // 因为题目要求的下标从一开始,所以申请 n + 1 的数组长度
    // a 数组用作保存输入数组
    a := make([]int, n + 1)
    // rk 保存数组 a 排序后的下标,也就是 a[rk[i]] 的结果是升序的
    rk := make([]int, n + 1)
    // 遍历保存每一个数组元素
    for i := 1; i <= n; i++ {
        fmt.Scanf("%d", &a[i])
        rk[i] = i
    }
    
    // 自定义排序算法,使得 a[rk[i]] 的结果升序
    sort.Slice(rk[1:], func(i, j int) bool {
        return a[rk[i + 1]] < a[rk[j + 1]]
    })
    
    
    // 定义双向链表
    // 选在在最小节点减 1e10,最大节点加 1e10 是因为防止在相邻节点计算时选到头尾节点
    // 也可以选择用 1e9 + 1 代替 1e10
    head, tail := &node{val: a[rk[1]]  - 1e10}, &node{val: a[rk[n]] + 1e10}
    head.next, tail.pre = tail, head
    
    // 定义一个 map,key 为数组 a 下标,val 为对应的节点指针地址
    mp := make(map[int]*node, n)
    
    // 遍历 rk,将 rk[i] 顺序插入链表
    // 插入后,链表从 head 到 tail, val 的值都递增
    for i := n; i >=1; i-- {
        idx := rk[i]
        cur := &node{
            idx: idx,
            val: a[idx],
        }
        mp[idx] = cur
        addNode(head, cur)
    }
    
    res := make([]int, n + 1)
    
    // 从后往前遍历数组,每次使用后将对应的链表节点删除
    // 因为根据题意,后面的数组元素不能影响前面的数组元素
    for i := n; i > 1; i-- {
        // 取出当前节点
        cur := mp[i]
        // 根据题意选择一个和当前值相减绝对值最小的
        // 因为链表已经按顺序排列了,所以只要相邻节点相减
        // 如果减出来绝对值都一样,则选择较小的那个值(由题意得)
        if cur.val - cur.pre.val <= cur.next.val - cur.val {
            res[i] = cur.pre.idx
        } else {
            res[i] = cur.next.idx
        }
        deleteNode(cur)
        delete(mp, i)
    }
    
    // 输出结果
    for i := 2; i <=n; i++ {
        fmt.Println(abs(a[res[i]] - a[i]), res[i])
    }
}

// abs 绝对值。
func abs(a int) int {
    if a < 0 {
        return -a
    }
    return a
}
  • 时间复杂度:O(nlogn),因为使用了 golang 内置的排序算法,此排序算法的时间复杂度约为 O(nlogn), 此外的操作基本都是 O(n) 级别的遍历操作,不涉及嵌套, 所以总体时间复杂度为 O(nlogn)。
  • 空间复杂度:O(n),因为使用了数个长度为 n 的辅助空间(数组和集合),所以空间复杂度为 O(n)。