Вопрос интервью: удалить дубликаты из несортированного связанного списка

Я читаю взлом кодирования интервью, четвертое издание: 150 Программирование интервью вопросы и решения и я пытаюсь решить следующий вопрос:

2.1 напишите код для удаления дубликатов из несортированного связанного списка. СЛЕДОВАТЬ UP: как бы вы решили эту проблему, если временный буфер не разрешен?

Я решаю его на C#, поэтому я сделал свой собственный Node класс:

public class Node<T> where T : class
{
    public Node<T> Next { get; set; }
    public T Value { get; set; }

    public Node(T value)
    {
        Next = null;
        Value = value;
    }
}

мое решение повторите список, затем для каждого узла повторите оставшуюся часть списка и удалите все дубликаты (обратите внимание, что я фактически не скомпилировал или не протестировал это, как указано в книге):

public void RemoveDuplicates(Node<T> head)
{
    // Iterate through the list
    Node<T> iter = head;
    while(iter != null)
    {
        // Iterate to the remaining nodes in the list
        Node<T> current = iter;
        while(current!= null && current.Next != null)
        {
            if(iter.Value == current.Next.Value)
            {
                current.Next = current.Next.Next;
            }

            current = current.Next;
        }    

        iter = iter.Next;
    }
}

вот решение из книги (автор написал его на java):

без буфера, мы можем пройтись с два указателя: "current" делает нормальный итерация, в то время как "runner" повторяет через все предыдущие узлы для проверки пакет DUP. Бегун только один dup в узел, потому что если было несколько дубликаты они бы уже снято.

public static void deleteDups2(LinkedListNode head) 
{
    if (head == null) return;

    LinkedListNode previous = head;
    LinkedListNode current = previous.next;

    while (current != null) 
    {
        LinkedListNode runner = head;

        while (runner != current) { // Check for earlier dups
            if (runner.data == current.data) 
            {
                LinkedListNode tmp = current.next; // remove current
                previous.next = tmp;
                current = tmp; // update current to next node
                break; // all other dups have already been removed
            }
            runner = runner.next;
        }
        if (runner == current) { // current not updated - update now
            previous = current;
            current = current.next;
        }
    }
}

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

11 ответов


Если вы дадите человеку рыбу, они едят в течение дня. Если научить человека рыбачить...

мои меры по качеству реализации:

  • достоверность: если вы не получаете правильный ответ во всех случаях, то он не готов
  • читаемость/ремонтопригодность: посмотрите на повторение кода, понятные имена, количество строк кода на блок / метод (и количество вещей, которые делает каждый блок), и как трудно отследить поток вашего кода. Посмотрите на любое количество книг, посвященных рефакторингу, лучшим методам программирования, стандартам кодирования и т. д., Если вы хотите получить больше информации об этом.
  • теоретическая производительность (в худшем случае и ammortized): Big-O - это метрика, которую вы можете использовать. CPU и потребление памяти должны быть измерены
  • сложности: оцените, как это займет средний профессиональный программист для реализации (если они уже знают алгоритм). Посмотрите, соответствует ли это тому, насколько трудна проблема на самом деле

Что касается вашей реализации:

  • достоверность: я предлагаю написать модульные тесты, чтобы определить это для себя и/или отладить его (на бумаге) от начала до конца с интересными примерами / крайними случаями. Null, один элемент, два элемента, различные числа дубликатов и т. д.
  • читаемость/ремонтопригодность: это выглядит в основном нормально, хотя ваши последние два комментария ничего не добавляют. Немного более очевидно, что делает ваш код, чем код в книге
  • производительность: я считаю, что оба N-квадрат. Является ли амортизированная стоимость ниже на одном или другом, я позволю вам выяснить:)
  • срок реализации: средний профессионал должен уметь кодировать этот алгоритм во сне, поэтому хорошо выглядит

нет большой разницы. Если я правильно сделал математику, ваши в среднем N / 16 медленнее, чем авторы, но существует множество случаев, когда ваша реализация будет быстрее.

Edit:

Я назову вашу реализацию Y, а автора a

оба предложенных решения имеют o(n^2) в худшем случае, и оба они имеют лучший случай O (N), когда все элементы имеют одинаковое значение.

EDIT: Это полный переписывать. Вдохновленный дебатом в комментариях, я попытался найти средний случай для случайных N случайных чисел. Это последовательность со случайным размером и случайным распределением. Каков будет средний случай.

Y всегда будет работать U раз, где U-количество уникальных чисел. Для каждой итерации он будет делать N-x сравнения, где X-количество элементов, удаленных до итерации (+1). В первый раз ни один элемент не будет удален и в среднем на второй итерации N / U будет удален.

то есть в среднем ½N останется для итерации. Мы можем выразить среднюю стоимость, U * ½N. Среднее значение U может быть выражено на основе N, а также 0

выражение A становится более сложным. Предположим, мы используем итерации I, Прежде чем встретим все уникальные значения. После этого будет выполняться между сравнениями 1 и U (в среднем это U/") и будет делать это N-I раз.

I * c+U / 2 (N-I)

но какое среднее число сравнения (c) мы запускаем для первых I итераций. в среднем нам нужно сравнить с половиной элементов, которые мы уже посетили, и в среднем мы посетили I / 2 элемента, т. е. c=I / 4

I/4+U / 2(N-I).

Я могу быть выражен в терминах N. В среднем нам нужно будет посетить половину на N, чтобы найти уникальные значения, поэтому I=N / 2 дает среднее значение

(I^2)/4+U/2 (N-I), который можно свести к (3*N^2)/16.

Это, конечно, если моя оценка средние значения верны. То есть в среднем для любой потенциальной последовательности A имеет N/16 меньше сравнений, чем Y, но существует множество случаев, когда Y быстрее A. Поэтому я бы сказал, что они равны по сравнению с количеством сравнений


Как насчет использования HashMap? Таким образом, это займет O(n) время и o(n) пространство. Я напишу psuedocode.

function removeDup(LinkedList list){
  HashMap map = new HashMap();
  for(i=0; i<list.length;i++)
      if list.get(i) not in map
        map.add(list.get(i))
      else
        list.remove(i)
      end
  end
end

конечно, мы предполагаем, что HashMap имеет O(1) Чтение и запись.

другим решением является использование mergesort и удаление дубликатов из начала и конца списка. Это занимает O (N log n)

mergesort - O (N log n) удаление дубликата из отсортированного списка равно O (n). знаешь почему? поэтому вся операция принимает O (N log n)


heapsort как - это на месте сортировка. Вы можете изменить функцию "siftUp" или "siftDown", чтобы просто удалить элемент, если он сталкивается с равным родителем. Это будет O (N log n)

function siftUp(a, start, end) is
 input:  start represents the limit of how far up the heap to sift.
               end is the node to sift up.
 child := end 
 while child > start
     parent := floor((child - 1) ÷ 2)
     if a[parent] < a[child] then (out of max-heap order)
         swap(a[parent], a[child])
         child := parent (repeat to continue sifting up the parent now)
     else if a[parent] == a[child] then
         remove a[parent]
     else
         return

ваше решение так же хорошо, как и авторское, только у него есть ошибка в реализации :) попробуйте проследить его по списку из двух узлов с равными данными.


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

интервью вопросы сделаны, чтобы проверить вашу открытость. У меня есть сомнения в ответе Марка: это определенно лучшее решение в примеры реального мира, но даже если эти алгоритмы используют постоянное пространство, ограничение, которое нет временный буфер разрешен должен соблюдаться.

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

в любом случае, чтобы углубиться в этот вопрос, ваш и подход книги требуют Theta(n^2) время, в то время как подход Марка требует Theta(n logn) + Theta(n) время, которое приводит к Theta(n logn). Почему? Theta? Потому что алгоритмы сравнения-свопа являются Omega(n logn) тоже помню!


код на java:

public static void dedup(Node head) {
    Node cur = null;
    HashSet encountered = new HashSet();

    while (head != null) {
        encountered.add(head.data);
        cur = head;
        while (cur.next != null) {
            if (encountered.contains(cur.next.data)) {
                cur.next = cur.next.next;
            } else {
                break;
            }
        }
        head = cur.next;
    }
}

пробовал то же самое в cpp. Пожалуйста, дайте мне знать ваши комментарии по этому поводу.

// ConsoleApplication2.cpp : определяет точку входа для консольного приложения. //

#include "stdafx.h"
#include <stdlib.h>
struct node
{
    int data;
    struct node *next;
};
struct node *head = (node*)malloc(sizeof(node));
struct node *tail = (node*)malloc(sizeof(node));

struct node* createNode(int data)
{
    struct node *newNode = (node*)malloc(sizeof(node));
    newNode->data = data;
    newNode->next = NULL;
    head = newNode;
    return newNode;
}

bool insertAfter(node * list, int data)
{
    //case 1 - insert after head
    struct node *newNode = (node*)malloc(sizeof(node));
    if (!list)
    {

        newNode->data = data;
        newNode->next = head;
        head = newNode;
        return true;
    }

    struct node * curpos = (node *)malloc(sizeof(node));
    curpos = head;
    //case 2- middle, tail of list
    while (curpos)
    {
        if (curpos == list)
        {
            newNode->data = data;
            if (curpos->next == NULL)
            {
            newNode->next = NULL;
            tail = newNode;
            }
            else
            {
                newNode->next = curpos->next;
            }
            curpos->next = newNode;
            return true;
        }
        curpos = curpos->next;
    }
}

void deleteNode(node *runner, node * curr){

    //DELETE AT TAIL
    if (runner->next->next == NULL)
    {
        runner->next = NULL;        
    }
    else//delete at middle
    {
        runner = runner->next->next;
        curr->next = runner;
    }
    }


void removedups(node * list)
{
    struct node * curr = (node*)malloc(sizeof(node));
    struct node * runner = (node*)malloc(sizeof(node));
    curr = head;
    runner = curr;
    while (curr != NULL){
        runner = curr;
        while (runner->next != NULL){
            if (curr->data == runner->next->data){
                deleteNode(runner, curr);
            }
            if (runner->next!=NULL)
            runner = runner->next;
        }
        curr = curr->next;
    }
}
int _tmain(int argc, _TCHAR* argv[])
{
    struct node * list = (node*) malloc(sizeof(node));
    list = createNode(1);
    insertAfter(list,2);
    insertAfter(list, 2);
    insertAfter(list, 3);   
    removedups(list);
    return 0;
}

код в C:

    void removeduplicates(N **r)
    {
        N *temp1=*r;
        N *temp2=NULL;
        N *temp3=NULL;
        while(temp1->next!=NULL)
        {
            temp2=temp1;
            while(temp2!=NULL)
            {
                temp3=temp2;
                temp2=temp2->next;
                if(temp2==NULL)
                {
                    break;
                }
                if((temp2->data)==(temp1->data))
                {
                    temp3->next=temp2->next;
                    free(temp2);
                    temp2=temp3;
                    printf("\na dup deleted");
                }
            }
            temp1=temp1->next;
        }

    }

вот ответ в C

    void removeduplicates(N **r)
    {
        N *temp1=*r;
        N *temp2=NULL;
        N *temp3=NULL;
        while(temp1->next!=NULL)
        {
            temp2=temp1;
            while(temp2!=NULL)
            {
                temp3=temp2;
                temp2=temp2->next;
                if(temp2==NULL)
                {
                    break;
                }
                if((temp2->data)==(temp1->data))
                {
                    temp3->next=temp2->next;
                    free(temp2);
                    temp2=temp3;
                    printf("\na dup deleted");
                }
            }
            temp1=temp1->next;
        }

    }

C# код для удаления дубликатов осталось после первой итерации:

 public Node removeDuplicates(Node head) 
    {
        if (head == null)
            return head;

        var current = head;
        while (current != null)
        {
            if (current.next != null && current.data == current.next.data)
            {
                current.next = current.next.next;
            }
            else { current = current.next; }
        }

        return head;
    }