排序链接列表:优化的元素添加/删除/重新排序

时间:2013-07-21 16:07:37

标签: c++ optimization linked-list a-star

我正在基于维基百科的伪代码实现A *寻路算法的实现,我正在尝试优化性能。我注意到大部分时间浪费在处理“开放”节点集中的节点上(基本上是要检查的节点),所以我一直在尽力使它更快。

(背景,随意跳转到代码) 基本上,我有一组节点,我用它做的是:

  • 找到得分最低的节点
  • 检查集合中是否包含节点
  • 从集合中删除节点
  • 将节点添加到集合

通过使用已排序的链接列表,找到最低分数从检查所有元素到获取第一个元素。添加节点变得更加复杂,但通常不需要遍历整个列表,因此节省了时间。无论如何,删除是相同的。为了检查节点是否在集合中,我使用具有直接访问权限的阵列映射,因此可以以一些额外的内存为代价快速完成。

class OpenNodeSet
{
public:
    OpenNodeSet(void);
    ~OpenNodeSet(void);

    void add(GraphNode *n);
    void remove(GraphNode *n);
    void reinsert(GraphNode *n);
    bool contains(GraphNode *n);
    GraphNode* top(void);

    int size(void)          {   return elements;    }
    bool empty(void)        {   return (elements == 0); }

private:
    typedef struct listNode {
        GraphNode *node;
        struct listNode *next;
    } listNode_t;

    listNode_t *root;
    int elements;
    unsigned __int8 map[1024][1024];
};

OpenNodeSet::OpenNodeSet(void)
{
    root = NULL;
    elements = 0;
    for (int i = 0; i < 1024; i++)
        for (int j = 0; j < 1024; j++)
            map[i][j] = 0;
}

OpenNodeSet::~OpenNodeSet(void)
{
    while (root != NULL) {
        listNode_t *tmp = root->next;
        free(root);
        root = tmp;
    }
}

void OpenNodeSet::add(GraphNode *n)
{
    listNode_t **head;
    for (head = &root; *head != NULL; head = &(*head)->next)
        if ((*head)->node->f_score > n->f_score)
            break;

    listNode_t *newNode = (listNode_t*)malloc(sizeof(listNode_t));
    newNode->node = n;
    newNode->next = *head;
    *head = newNode;
    map[n->x][n->y] = 1;
    elements++;
}

void OpenNodeSet::remove(GraphNode *n)
{
    listNode_t **head;
    for (head = &root; *head != NULL; head = &(*head)->next)
        if ((*head)->node == n) {
            listNode_t *tmp = *head;
            *head = (*head)->next;
            free(tmp);
            map[n->x][n->y] = 0;
            elements--;
            return;
        }
}

void OpenNodeSet::reinsert(GraphNode *n)
{
    listNode_t **head, **bookmark = NULL;
    listNode_t *ln = NULL;
    int pos = 0;
    for (head = &root; *head != NULL; head = &(*head)->next, pos++) {
        if (bookmark == NULL && (*head)->node->f_score > n->f_score)
            bookmark = head;
        if (ln == NULL && (*head)->node == n) {
            ln = *head;
            *head = (*head)->next;
        }
        if (bookmark && ln)
            break;
    }

    ln->next = (*bookmark)->next;
    *bookmark = ln;
}

bool OpenNodeSet::contains(GraphNode *n)
{
    return map[n->x][n->y]==1;
}

GraphNode* OpenNodeSet::top(void)
{
    return root->node;
}

代码不像在某些地方那样干净/高效,现在我只是处于工作模式。问题是,它不起作用。

重新插入功能在那里,因为我经常需要更改节点的分数,从而也改变它在列表中的位置。我没有使用一个列表遍历来移除它而另一个用于插入它,而是使用单个遍历来找到重新插入节点的位置,以及节点本身应该节省我分配内存和另一个遍历。问题是,有时重新插入功能无法找到要重新插入的节点,也就是说,节点不存在(并且它没有,我已手动检查它)。但这不应该发生。如果我忽略它,最终我会得到一个分段错误(或Visual Studio中的等价物),但我不确定这是否只是忽略它的结果。这是查找路径的功能:

void Graph::findPath(int start_x, int start_y, int goal_x, int goal_y)
{
    GraphNode *start = map[start_x][start_y];
    GraphNode *goal = map[goal_x][goal_y];
    OpenNodeSet open;
    NodeSet closed;

    open.add(start);
    start->g_score = 0;
    start->f_score = distance(start, goal);

    while (!open.empty()) {
        //FIND MIN F_SCORE NODE AND REMOVE NODE FROM OPEN LIST
        GraphNode *curr = open.top();
        open.remove(curr);

        //REACHED GOAL?
        if (curr == goal) {
            reconstructPath(curr);
            break;
        }

        //ADD MIN COST NODE TO CLOSED LIST
        closed.add(curr);

        //FOR EACH NEIGHBOR OF NODE
        for (int i = 0; i < curr->neighbor_count; i++) {
            GraphNode *neighbor = curr->neighbors[i].node;
            float cost = curr->neighbors[i].cost;

            float tmp_g = curr->g_score + cost;
            if (closed.contains(neighbor) && tmp_g >= neighbor->g_score)
                continue;

            bool inOpenSet = open.contains(neighbor);
            if (!inOpenSet || tmp_g < neighbor->g_score) {
                neighbor->parent = curr;
                neighbor->g_score = tmp_g;
                neighbor->f_score = tmp_g + distance(neighbor, goal);

                if (inOpenSet)
                    open.reinsert(neighbor);
                else
                    open.add(neighbor);
            }
        }
    }
}

我的列表实现显然正在进行,但我无法弄清楚是什么。当我尝试用其他值测试它时,它的行为就像它应该的那样。我不知道reinsert()是否应该如此工作,因为它不应该输入它。有趣的是,如果我使用以下内容代替reinsert()/add()来电:

        if (inOpenSet) {
            open.remove(neighbor);
        }
        open.add(neighbor);

......事情似乎很好。我甚至检查过,在某些时候删除了一个不存在的元素,显然,它不是。这让我怀疑reinsert()功能,但我并不像我想的那样确定。据我所知,使用删除/添加可能只是使程序工作,但给出不正确的结果。无论哪种方式,我一直盯着自己,需要另一种观点。

有人能看到这里发生了什么吗?这似乎是某种特殊情况,因为问题很罕见,但我无法重现它。当我使用人工值进行测试时,reinsert()可以毫无怨言地完成我的预期。

PS。任何其他优化技巧或比节点之间的距离更好的启发式成本估算也是受欢迎的。大多数情况下,这个特定部分都是关于速度而不是内存使用情况,但两者都很好。哦,Stack Overflow童贞不见了。

修改

结果是一顿美味的晚餐,我只需要几个小时的闲暇时间。最后两行reinsert()应该读取:

ln->next = *bookmark;
*bookmark = ln;

我不确定当我测试它时是否做得对,并且没有注意到代码不匹配,但是你去了。与常规删除+添加操作相比,将在特定测试用例中重新评分的节点修复所花费的时间减少了25%。

但是,关于如何让它变得更好的更多想法总是受欢迎的。我希望能够进行二进制搜索以找到插入位置,但我无法想到一个好方法,而不必在巨大的固定大小数组或常量realloc周围铲掉大量内存()来电。

0 个答案:

没有答案