Как правильно вставить/удалить в двоичном дереве поиска в C?

Я как бы должен отложить свои предыдущие вопросы C, потому что этот вопрос сейчас более важен...

Я уже закодировал функции вставки и удаления в моем двоичном дереве поиска, но функция удаления не завершена. Мне нужна помощь в паре вещей...

1) Моя функция вставки хороша или ее можно как-то улучшить?

2) В моей функции удаления отсутствует удаление узла как с левым, так и с правым дочерними элементами. Я много искал за последние несколько часов, но не смог найти правильный способ сделать это.

2.a) Как удалить узел с двумя дочерними узлами?

2.b) Как и в первом вопросе, хороша ли функция удаления или ее можно улучшить? Это, я знаю, может, потому что я повторяю много кода в этих ifs, но я не вижу, как я могу его улучшить, мне тоже нужна помощь.

typedef struct sClientProfile *ClientProfile;
typedef struct sClientTree *ClientTree;

typedef struct sClientProfile {
    char *clientName;
    int clientAge;
    int clientNIF;
} nClientProfile;

typedef struct sClientTree {
    ClientProfile clientProfile;
    char *clientName;

    ClientTree leftTree;
    ClientTree rightTree;
} nClientTree;

void addClientToTree(ClientTree *cTree, ClientProfile cProfile) {
    if(!*cTree) {
        ClientTree new = (ClientTree)malloc(sizeof(nClientTree));

        if(!new) {
            perror("malloc");
        }

        new->clientName = strdup(cProfile->clientName);
        new->clientProfile = cProfile;

        new->leftTree = NULL;
        new->rightTree = NULL;

        *cTree = new;
    } else {
        if(strcmp((*cTree)->clientName, cProfile->clientName) > 0) {
            addClientToTree(&(*cTree)->leftTree, cProfile);
        } else {
            addClientToTree(&(*cTree)->rightTree, cProfile);
        }
    }
}

void deleteClientFromTree(ClientTree *cTree, char *cName) {
    if(!cTree) return;

    int nCompare = strcmp((*cTree)->clientName, cName);

    if(nCompare > 0) {
        deleteClientFromTree(&(*cTree)->leftTree, cName);
    } else if(nCompare < 0) {
        deleteClientFromTree(&(*cTree)->rightTree, cName);
    } else {
        if(!(*cTree)->leftTree && !(*cTree)->rightTree) {
            ClientTree cliPtr = *cTree;

            free(cliPtr->clientProfile);
            free(cliPtr);

            cliPtr->clientProfile = NULL;
            cliPtr = NULL;

            *cTree = NULL;
        } else if(!(*cTree)->leftTree) {
            ClientTree cliPtr = *cTree;

            free(cliPtr->clientProfile);
            free(cliPtr);

            cliPtr->clientProfile = NULL;

            *cTree = (*cTree)->rightTree;
        } else if(!(*cTree)->rightTree) {
            ClientTree cliPtr = *cTree;

            free(cliPtr->clientProfile);
            free(cliPtr);

            cliPtr->clientProfile = NULL;

            *cTree = (*cTree)->leftTree;
        } else {

            // MISSING DELETE CASE

        }
    }
}

Вы, наверное, заметили, но позвольте мне сделать два замечания:

  • Это дерево использует строки вместо обычного представления типа int. Вот почему я всегда использую strcmp(), я думаю, что использую ее правильно.
  • Я не использую рекурсию, я скорее передаю указатель (в данном случае указателя структуры) и работаю с ним. Как-то это выглядит более чисто, и в будущем я хочу вернуть значение успеха, если узел был удален.

ОБНОВЛЕНИЕ НИЖЕ:
Я уже сделал свою итеративную версию функции удаления, но мне не нравятся некоторые вещи в ней, может быть, их можно улучшить (или нет), но я не могу Смотри как. Я также пытался закодировать случай, когда он отсутствовал, удаляя узел с двумя дочерними элементами, но он не работает должным образом...

Я прокомментировал весь код, где, по моему мнению, код можно улучшить и в чем проблема. Я также назвал эти проблемы как A, B (Б больше нет), C и D, чтобы мы могли легко ссылаться на них.

bool deleteClientFromTree(ClientTree *cTree, char *cName) {
    if(!cTree) return FALSE;

    ClientTree currPtr = *cTree;
    ClientTree prevPtr = NULL;
    int nCompare;

    while(currPtr) {
        nCompare = strcmp(currPtr->clientName, cName);

        if(nCompare > 0) {
            prevPtr = currPtr;
            currPtr = currPtr->leftTree;
        } else if(nCompare < 0) {
            prevPtr = currPtr;
            currPtr = currPtr->rightTree;
        } else {
            /*
             * A)
             * 
             * The following cases have 3 lines in common, the free()
             * calls and return statement. Is there anyway to improve
             * this code and make it more compact?
             * 
             * Of course, the printf's are to be removed...
             */
            if(!prevPtr && !currPtr->leftTree && !currPtr->rightTree) {
                printf("CASE #1\n");

                *cTree = NULL;

                free(currPtr->clientProfile);
                free(currPtr);

                return TRUE;
            } else if(!currPtr->leftTree || !currPtr->rightTree) {
                printf("CASE #2\n");

                if(prevPtr->leftTree == currPtr) {
                    prevPtr->leftTree = currPtr->rightTree;
                } else {
                    prevPtr->rightTree = currPtr->leftTree;
                }

                free(currPtr->clientProfile);
                free(currPtr);

                return TRUE;
            } else {
                printf("CASE #3\n");

                ClientTree tempPtr = currPtr->rightTree;

                while(tempPtr->leftTree) {
                    tempPtr = tempPtr->leftTree;
                }

                /*
                 * C)
                 * 
                 * This has a big problem...
                 * 
                 * If you take a look at the ClientProfile structure,
                 * in the first post, you'll see two ints
                 * (clientNIF/clientAge) and one char* (clientName).
                 * 
                 * The problem is that the following code line is only
                 * copying the integer data, not the string. For some
                 * reason, the string remains the old one.
                 * 
                 * I tried to use strdup() directly on clientName like:
                 * currPtr->clientProfile->clientName = strdup(tempPtr->clientProfile->clientName);
                 * but it still doesn't work.
                 * 
                 * Why everything is being copied but the strings?
                 */
                currPtr->clientProfile = tempPtr->clientProfile;

                /*
                 * D)
                 * 
                 * Is there anyway to not call the function itself
                 * and make the while loop once again and delete the
                 * corresponding leaf?
                 */
                return deleteClientFromTree(&currPtr->rightTree, tempPtr->clientProfile->clientName);
            }
        }
    }

    return FALSE;
}

person rfgamaral    schedule 24.03.2009    source источник
comment
Не преобразовывайте возвращаемое значение malloc() в C. Оно никогда не понадобится и может скрыть ошибку. Кроме того, ваша проверка ошибок непоследовательна, вы проверяете malloc(), но не strdup(). Просто предупреждение, чтобы не забыть починить. Я бы также не стал набирать указатели, но, возможно, это скорее вкусовщина.   -  person unwind    schedule 24.03.2009
comment
Мне нравится указывать указатели :) Спасибо за другие советы.   -  person rfgamaral    schedule 24.03.2009
comment
Скрывать указатели в typedef тоже некрасиво.   -  person user562374    schedule 11.02.2011
comment
Сокрытие указателей в typedef может быть очень проблематичным позже.   -  person asheeshr    schedule 23.11.2012


Ответы (5)


Когда вы удаляете узел, вы должны что-то делать с его дочерними элементами.

Если нет детей - не проблема. Вы просто удаляете узел.

Если есть левый ребенок, тоже без проблем; вы удаляете узел и перемещаете его левого потомка на его место.

То же самое для правого ребенка; просто переместите ребенка на место удаленного узла.

Проблема возникает, когда вы хотите удалить узел, у которого есть как левый, так и правый потомки. Вы можете переместить левый или правый потомок на место удаленного узла, но что тогда делать с другим потомком и его поддеревом?

Решение таково; вы находите логического преемника удаляемого узла. Под логическим преемником я подразумеваю это; скажем, у вас есть дерево, состоящее из целых чисел, и вы удаляете узел со значением 35, логический преемник является следующим наибольшим числом. Я? если бы вы выполняли прогулку по порядку, это был бы элемент, к которому вы пришли после элемента, который вы удаляете.

Теперь есть простое правило для поиска логического преемника; вы идете направо (у вас всегда есть право, потому что это тот случай, когда у вас двое детей), а затем вы идете так далеко налево, как только можете.

Тот элемент, на котором вы остановились, является логическим преемником. Он больше, чем удаленный элемент (вы все сделали правильно с самого начала, помните?), но это наименьший следующий за наибольшим элемент.

Теперь этот элемент ВСЕГДА имеет только одного потомка или не имеет его вовсе — потому что вы шли влево так далеко, как только могли, помните? так что вы больше не можете идти влево - потому что левого нет - так что у этого элемента нет дочерних элементов или только правый дочерний элемент, и это означает, что он попадает в одну из легко удаляемых категорий (нет дочерних элементов или только один дочерний элемент) . Так что удалить этот элемент очень просто.

Теперь самое интересное - подумайте об этом; если бы этот следующий по величине элемент находился в том же месте в дереве, что и элемент, который вы хотите удалить, дерево по-прежнему было бы действительным и правильным, потому что все, что находится слева от каждого элемента, меньше, все до правый больше.

Итак, что вы делаете, это; вы копируете пользовательские данные из следующего по величине узла в удаляемый узел и удаляете следующий по величине узел (у него нет дочерних элементов или есть только правильный дочерний узел, поэтому его легко разъединить и удалить).

И это все!

Итак, в основном - найдите своего логического преемника, отсоедините его от дерева и поместите его пользовательские данные в элемент, который вы фактически изначально удаляете (который вы, конечно, не удаляете, потому что он все еще физически является частью дерева).

person Community    schedule 24.03.2009
comment
На самом деле все немного сложнее. Перемещение пользовательских данных из одного элемента в другой — это плохо, потому что что произойдет, если у пользователя будет указатель на перемещаемый элемент данных (следующий по величине элемент)? Что вам действительно нужно сделать, так это физически заменить целевой элемент удаления следующим по величине элементом. На самом деле переместите следующий по величине элемент в дереве, а не просто скопируйте пользовательские данные. - person ; 23.02.2011

Во-первых, вы упомянули, что не используете рекурсию, но у каждой функции есть логический путь, который вызывает саму себя.

К вопросам:

1) Убрать рекурсию. Это может доставить вам много неприятностей, если ваше дерево достаточно велико, чтобы взорвать ваш стек. Gcc имеет ограниченную поддержку хвостовой рекурсии, но я бы на это не рассчитывал.

2) Как правило, когда вы удаляете дочерний элемент с двумя узлами, вы перемещаете левый или правый узел в положение, в котором находился удаленный узел. (Это очень упрощенный случай, я предполагаю, что ваше дерево не сбалансировано)

2.b) Ваш код удаления имеет некоторые проблемы. Я бы рекомендовал пройтись по нему с несколькими гипотетическими ситуациями. Для меня сразу стало очевидным освобождение указателя, а затем его отмена:

free(cliPtr);

cliPtr->clientProfile = NULL;

Конечно, вы всегда можете побеспокоиться о стиле, как только разберетесь с правильностью.

person matt_h    schedule 24.03.2009
comment
1) Я понятия не имею, как я могу закодировать это без рекурсии, так как мне нужно пройти 2 узла. 2) Он не сбалансирован, возможно, в будущем я так и сделаю. 2.b) Я не могу понять, в чем проблема... Я просто хочу убедиться, что это удалено. - person rfgamaral; 24.03.2009
comment
Я только что сделал функцию вставки итеративной, я попытаюсь сделать так, чтобы удалить ее было хорошо. Что касается разыменования, я предпочитаю именно так, чтобы у меня не было висящих указателей. Судя по статье в Википедии на эту тему, это был лучший вариант. - person rfgamaral; 24.03.2009
comment
Висячий указатель - это именно то, что у вас есть, когда вы освобождаете cliPtr, cliPtr теперь является указателем, указывающим на недопустимые данные. Как говорится в статье в википедии.. - person matt_h; 25.03.2009

В идеале существует три случая удаления узла в BST:

Дело 1:

X has no children: remove X

Случай 2:

X has one children : Splice out X

Случай 3:

X has two children : swap X with its successor and follow case #1 or #2

Итак, для отсутствующего случая удаления:

Когда X (удаляемый узел) имеет двух дочерних элементов, замените X преемником X и выполните вариант № 1 или вариант № 2. Вы также можете заменить его предшественником, может быть хорошей альтернативой.

if ( X->left && X->right) {

NODE *Successor = FindSuccessor(X);
X->data = Successor->data;
free(Successor);

}

person Ganesh M    schedule 24.03.2009
comment
Это все хорошо, но это почти только теория, как и все, что я уже читал, и вот чего я не понимаю, я, должно быть, тупой... Какой преемник? А предшественник? Почему я должен идти с одним вместо другого? - person rfgamaral; 24.03.2009

это двоичные коды вставки, удаления, поиска и выхода. Примеры:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Binary Tree {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        LinkedList ll = new LinkedList();

        ll.add("\n"+"mai    0020");
        ll.add("\n"+"king   0019");
        ll.add("\n"+"maan   0002");
        ll.add("\n"+"dimple 0024");
        ll.add("\n"+"eman   0004");
        ll.add("\n"+"lara   0005");
        ll.add("\n"+"cute   0008");
        ll.add("\n"+"irene  0011");
        ll.add("\n"+"sheena 0030");
        ll.add("\n"+"aisy   0003");

        System.out.println("display: " + ll);
        System.out.println("\n\n");

        for(int c=0; c<=10; c++) {
            System.out.println("select from: 1-insert, 2-delete," + 
                               " 3-display, 4-search, 5-quit");

            String x = br.readLine();
            int y = Integer.parseInt(x);
            switch (y) {
                case 1: //inserting
                    System.out.println("input name");
                    String n= br.readLine();
                    System.out.println("input a list number");
                    String o = br.readLine();
                    int z = Integer.parseInt(o);
                    ll.add("\n"+n+" "+z);
                    break;
                case 2: // delete   
                    ll.removeFirst();
                    break;
                case 3: //Display
                    System.out.println("\n\n"+"List of employee: " + ll);       
                    System.out.println("\n\n");
                    break;
                case 4: //search
                    System.out.println("\n");
                    System.out.println("Search");

                    System.out.println("first element of the Linkedlist is: "
                                       + ll.getFirst());
                    System.out.println("\n\n");
                    System.out.println("last element of linkedlist:"
                                       + ll.getLast());     
                    break;
                case 5: //quit
                    System.out.println("\n\n\n\n\n" 
                                       + " Thank You Very Much!!!!!!!\n\n\n");
                    System.exit(0);
                    break;
            }
        }
    }
}
person Greggy    schedule 11.02.2011

person    schedule
comment
Вместо того, чтобы просто публиковать блок кода, пожалуйста, объясните, почему этот код решает поставленную проблему. Без объяснения это не ответ. - person Martijn Pieters; 09.12.2012